From 8edb62253fc70148a65196ef3716a45ec77f6e33 Mon Sep 17 00:00:00 2001 From: irfanuddinahmad <34648393+irfanuddinahmad@users.noreply.github.com> Date: Mon, 3 Jul 2023 15:56:17 +0500 Subject: [PATCH 001/124] feat: Added column for course product line in learner credit table (#996) Co-authored-by: IrfanUddinAhmad --- .../LearnerCreditAllocationTable.jsx | 7 +++ .../learner-credit-management/data/utils.js | 1 + .../LearnerCreditAllocationTable.test.jsx | 50 +++++++++++++++++++ src/utils.js | 7 +++ 4 files changed, 65 insertions(+) create mode 100644 src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx diff --git a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx index 081eff5946..12eada0677 100644 --- a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx +++ b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx @@ -7,6 +7,7 @@ import moment from 'moment'; import TableTextFilter from './TableTextFilter'; import EmailAddressTableCell from './EmailAddressTableCell'; +import { getCourseProductLineText } from '../../utils'; export const PAGE_SIZE = 20; export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array @@ -52,6 +53,11 @@ const LearnerCreditAllocationTable = ({ Cell: ({ row }) => moment(row.values.enrollmentDate).format('MMMM DD, YYYY'), disableFilters: true, }, + { + Header: 'Product', + accessor: 'courseProductLine', + Cell: ({ row }) => getCourseProductLineText(row.values.courseProductLine), + }, ]} initialTableOptions={{ getRowId: row => row?.uuid?.toString(), @@ -89,6 +95,7 @@ LearnerCreditAllocationTable.propTypes = { courseTitle: PropTypes.string.isRequired, courseListPrice: PropTypes.number.isRequired, enrollmentDate: PropTypes.string.isRequired, + courseProductLine: PropTypes.string.isRequired, })), itemCount: PropTypes.number.isRequired, pageCount: PropTypes.number.isRequired, diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 0ba0628302..7a9101ba16 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -58,6 +58,7 @@ export const transformUtilizationTableResults = results => results.map(result => courseTitle: result.courseTitle, courseListPrice: result.courseListPrice, enrollmentDate: result.enrollmentDate, + courseProductLine: result.courseProductLine, uuid: uuidv4(), })); diff --git a/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx new file mode 100644 index 0000000000..00ebf8f3fc --- /dev/null +++ b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { + screen, + render, +} from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import LearnerCreditAllocationTable from '../LearnerCreditAllocationTable'; + +const LearnerCreditAllocationTableWrapper = (props) => ( + + + +); + +describe('', () => { + it('renders with table data', () => { + const props = { + enterpriseUUID: 'test-enterprise-id', + isLoading: false, + tableData: { + results: [{ + userEmail: 'test@example.com', + courseTitle: 'course-title', + courseListPrice: 100, + enrollmentDate: '2-2-23', + courseProductLine: 'OCM', + }], + itemCount: 1, + pageCount: 1, + }, + fetchTableData: jest.fn(), + }; + props.fetchTableData.mockReturnValue(props.tableData); + + render(); + + expect(screen.getByText('Open', { exact: false })); + expect(screen.getByText(props.tableData.results[0].userEmail.toString(), { + exact: false, + })); + expect(screen.getByText(props.tableData.results[0].courseTitle.toString(), { + exact: false, + })); + expect(screen.getByText(props.tableData.results[0].courseListPrice.toString(), { + exact: false, + })); + expect(screen.getByText('February', { exact: false })); + }); +}); diff --git a/src/utils.js b/src/utils.js index 6fe486e35c..f92dff63b7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -400,6 +400,12 @@ const pollAsync = async (pollFunc, timeout, interval, checkFunc) => { return false; }; +const getCourseProductLineText = (courseProductLine) => { + let courseProductLineText = ''; + courseProductLineText = courseProductLine === 'OCM' ? 'Open Courses' : courseProductLine; + return courseProductLineText; +}; + export { camelCaseDict, camelCaseDictArray, @@ -433,4 +439,5 @@ export { capitalizeFirstLetter, pollAsync, isNotValidNumberString, + getCourseProductLineText, }; From f0ea4b015a1fa9a88a96bc572ffbd68e7972b24c Mon Sep 17 00:00:00 2001 From: Zaman Afzal Date: Mon, 10 Jul 2023 16:30:52 +0500 Subject: [PATCH 002/124] refactor: fix the subscription table label value on LPR (#1002) --- src/components/Admin/licenses/LicenseAllocationHeader.jsx | 3 ++- .../__snapshots__/LicenseAllocationHeader.test.jsx.snap | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Admin/licenses/LicenseAllocationHeader.jsx b/src/components/Admin/licenses/LicenseAllocationHeader.jsx index c24001e321..b7ac4bbdb8 100644 --- a/src/components/Admin/licenses/LicenseAllocationHeader.jsx +++ b/src/components/Admin/licenses/LicenseAllocationHeader.jsx @@ -14,6 +14,7 @@ const LicenseAllocationHeader = () => { // don't show alert if the enterprise already has subsidy requests enabled const isBrowseAndRequestFeatureAlertShown = subsidyRequestConfiguration?.subsidyType === SUPPORTED_SUBSIDY_TYPES.license && !subsidyRequestConfiguration?.subsidyRequestsEnabled; + const activatedAndAssigned = (subscription.licenses?.activated ?? 0) + (subscription.licenses?.assigned ?? 0); return ( <> {isBrowseAndRequestFeatureAlertShown && } @@ -25,7 +26,7 @@ const LicenseAllocationHeader = () => { Activated: {subscription.licenses?.activated} {' of '} - {subscription.licenses?.assigned} assigned + {activatedAndAssigned} assigned diff --git a/src/components/Admin/licenses/__snapshots__/LicenseAllocationHeader.test.jsx.snap b/src/components/Admin/licenses/__snapshots__/LicenseAllocationHeader.test.jsx.snap index 04f310158d..8ca7a7337b 100644 --- a/src/components/Admin/licenses/__snapshots__/LicenseAllocationHeader.test.jsx.snap +++ b/src/components/Admin/licenses/__snapshots__/LicenseAllocationHeader.test.jsx.snap @@ -28,7 +28,7 @@ exports[`LicenseAllocationHeader renders without crashing 1`] = ` Activated: 1 of - 1 + 2 assigned From 107403e6ca96e5e6a1356889efde972dd34fd86d Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:26:35 -0700 Subject: [PATCH 003/124] test: add test coverage for sidebar component (#1005) * test: add test coverage for sidebar component * fix: removed exact option per reviewer comment --- src/containers/Sidebar/Sidebar.test.jsx | 45 ++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/containers/Sidebar/Sidebar.test.jsx b/src/containers/Sidebar/Sidebar.test.jsx index a77fe435d0..6ba0e531aa 100644 --- a/src/containers/Sidebar/Sidebar.test.jsx +++ b/src/containers/Sidebar/Sidebar.test.jsx @@ -45,6 +45,7 @@ const initialState = { enableCodeManagementScreen: true, enableSubscriptionManagementScreen: true, enableAnalyticsScreen: true, + enableReportingConfigScreenLink: true, }, }; @@ -269,7 +270,7 @@ describe('', () => { }); render(); - const subscriptionManagementLink = screen.queryByRole('link', { name: 'Subscription Management' }, { exact: false }); + const subscriptionManagementLink = screen.queryByRole('link', { name: 'Subscription Management' }); expect(subscriptionManagementLink).toBeNull(); }); @@ -283,8 +284,40 @@ describe('', () => { }, }); render(); - const subscriptionManagementLink = screen.getByRole('link', { name: 'Subscription Management' }, { exact: false }); + const subscriptionManagementLink = screen.getByRole('link', { name: 'Subscription Management' }); expect(subscriptionManagementLink).toBeInTheDocument(); + expect(subscriptionManagementLink).toHaveAttribute('href', '/test-enterprise-slug/admin/subscriptions'); + }); + + it('renders correctly when enableReportingConfigScreen is false', () => { + const store = mockStore({ + sidebar: { + ...initialState.sidebar, + }, + portalConfiguration: { + enableReportingConfigScreen: false, + }, + }); + features.REPORTING_CONFIGURATIONS = true; + render(); + const enableReportingConfigScreenLink = screen.queryByRole('link', { name: 'Reporting Configurations' }); + expect(enableReportingConfigScreenLink).toBeNull(); + }); + + it('renders correctly when enableReportingConfigScreen is enabled', async () => { + const store = mockStore({ + sidebar: { + ...initialState.sidebar, + }, + portalConfiguration: { + enableReportingConfigScreen: true, + }, + }); + features.REPORTING_CONFIGURATIONS = true; + render(); + const enableReportingConfigScreenLink = screen.getByRole('link', { name: 'Reporting Configurations' }); + expect(enableReportingConfigScreenLink).toBeInTheDocument(); + expect(enableReportingConfigScreenLink).toHaveAttribute('href', '/test-enterprise-slug/admin/reporting'); }); it('renders settings link if the settings page has visible tabs.', () => { @@ -298,7 +331,9 @@ describe('', () => { features.SETTINGS_PAGE = true; render(); - expect(screen.getByRole('link', { name: 'Settings' })).toBeInTheDocument(); + const settingsLink = screen.getByRole('link', { name: 'Settings' }); + expect(settingsLink).toBeInTheDocument(); + expect(settingsLink).toHaveAttribute('href', '/test-enterprise-slug/admin/settings'); }); it('renders manage learner credit link if the canManageLearnerCredit = true.', () => { @@ -310,7 +345,9 @@ describe('', () => { }); render(); - expect(screen.getByRole('link', { name: 'Learner Credit Management' })).toBeInTheDocument(); + const enableLearnerCreditLink = screen.getByRole('link', { name: 'Learner Credit Management' }); + expect(enableLearnerCreditLink).toBeInTheDocument(); + expect(enableLearnerCreditLink).toHaveAttribute('href', '/test-enterprise-slug/admin/learner-credit'); }); it('hides manage learner credit link if the canManageLearnerCredit = false.', () => { From a209dd019b545f8b7cea55d9f0b1d8739a6a1e45 Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:59:16 -0700 Subject: [PATCH 004/124] refactor: Replace useWindowSize with Paragon's useWindowSize (#1006) * refactor: Replace useWindowSize with Paragon's useWindowSize * refactor: applied changes per reviewer comment --- .../IconWithTooltip/IconWithTooltip.test.jsx | 9 +++-- src/components/IconWithTooltip/index.jsx | 9 +++-- .../forms/tests/ValidatedFormControl.test.tsx | 4 +-- src/hooks/index.js | 1 - src/hooks/useWindowSize.jsx | 35 ------------------- 5 files changed, 10 insertions(+), 48 deletions(-) delete mode 100644 src/hooks/useWindowSize.jsx diff --git a/src/components/IconWithTooltip/IconWithTooltip.test.jsx b/src/components/IconWithTooltip/IconWithTooltip.test.jsx index 6b7c996a98..c682c796fd 100644 --- a/src/components/IconWithTooltip/IconWithTooltip.test.jsx +++ b/src/components/IconWithTooltip/IconWithTooltip.test.jsx @@ -3,9 +3,6 @@ import { render, screen, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import IconWithTooltip from './index'; -import useWindowSize from '../../hooks/useWindowSize'; - -jest.mock('./../../hooks/useWindowSize'); const defaultAltText = 'infoooo'; const defaultTooltipText = 'Tooool'; @@ -17,7 +14,8 @@ const DEFAULT_PROPS = { describe('', () => { it('renders the icon passed to it with alt text', () => { - useWindowSize.mockReturnValue({ width: 800 }); + global.innerWidth = 800; + global.dispatchEvent(new Event('resize')); const { container } = render(); expect(container.querySelector(`[data-icon=${faInfoCircle.iconName}]`)).toBeTruthy(); }); @@ -26,7 +24,8 @@ describe('', () => { { windowSize: 700, expectedLocation: 'top' }, ].forEach((data) => { it(`renders the tooltip on the ${data.expectedLocation} for ${data.windowSize}px screen`, () => { - useWindowSize.mockReturnValue({ width: data.windowSize }); + global.innerWidth = data.windowSize; + global.dispatchEvent(new Event('resize')); const { container } = render(); const icon = container.querySelector(`[data-icon=${faInfoCircle.iconName}]`); expect(icon).toBeTruthy(); diff --git a/src/components/IconWithTooltip/index.jsx b/src/components/IconWithTooltip/index.jsx index 9797f5b4c4..67c99fe4fb 100644 --- a/src/components/IconWithTooltip/index.jsx +++ b/src/components/IconWithTooltip/index.jsx @@ -1,18 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { OverlayTrigger, Tooltip } from '@edx/paragon'; -import useWindowSize from '../../hooks/useWindowSize'; +import { OverlayTrigger, Tooltip, useWindowSize } from '@edx/paragon'; const IconWithTooltip = ({ icon, altText, tooltipText, placementSm = 'right', placementLg = 'top', trigger = ['hover', 'focus'], breakpoint = 768, iconClassNames = 'ml-1', }) => { - const windowSize = useWindowSize(); - const placement = windowSize.width >= breakpoint ? placementSm : placementLg; + const { width } = useWindowSize(); + const placement = width >= breakpoint ? placementSm : placementLg; return ( = breakpoint ? placementSm : placementLg} + placement={placement} data-testid={`tooltip-${placement}`} overlay={( diff --git a/src/components/forms/tests/ValidatedFormControl.test.tsx b/src/components/forms/tests/ValidatedFormControl.test.tsx index e3df1207c9..3328a45474 100644 --- a/src/components/forms/tests/ValidatedFormControl.test.tsx +++ b/src/components/forms/tests/ValidatedFormControl.test.tsx @@ -28,8 +28,8 @@ const ValidatedFormControlWrapper = ({ formFields: { [formId]: formValue }, }; if (formError) { - contextValue = - { ...contextValue, + contextValue = { + ...contextValue, errorMap: { [formId]: [formError] }, showErrors: true, }; diff --git a/src/hooks/index.js b/src/hooks/index.js index a82dfa94bc..9f02607bb4 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,3 +1,2 @@ export { default as useInterval } from './useInterval'; -export { default as useWindowSize } from './useWindowSize'; export { default as useOnMount } from './useOnMount'; diff --git a/src/hooks/useWindowSize.jsx b/src/hooks/useWindowSize.jsx deleted file mode 100644 index 002a74e29a..0000000000 --- a/src/hooks/useWindowSize.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState, useEffect } from 'react'; - -// Hook -function useWindowSize() { - // Initialize state with undefined width/height so server and client renders match - // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ - const [windowSize, setWindowSize] = useState({ - width: undefined, - height: undefined, - }); - - useEffect(() => { - // Handler to call on window resize - function handleResize() { - // Set window width/height to state - setWindowSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - } - - // Add event listener - window.addEventListener('resize', handleResize); - - // Call handler right away so state gets updated with initial window size - handleResize(); - - // Remove event listener on cleanup - return () => window.removeEventListener('resize', handleResize); - }, []); // Empty array ensures that effect is only run on mount - - return windowSize; -} - -export default useWindowSize; From af86f42eba87779a03d7e12891affb80ad68a84d Mon Sep 17 00:00:00 2001 From: jajjibhai008 Date: Mon, 10 Jul 2023 18:25:46 +0500 Subject: [PATCH 005/124] fix: do not require change password to change reporting configuration --- .../ReportingConfig/ReportingConfigForm.jsx | 6 ++-- .../ReportingConfigForm.test.jsx | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/components/ReportingConfig/ReportingConfigForm.jsx b/src/components/ReportingConfig/ReportingConfigForm.jsx index c2c9d8c804..4e662c6a11 100644 --- a/src/components/ReportingConfig/ReportingConfigForm.jsx +++ b/src/components/ReportingConfig/ReportingConfigForm.jsx @@ -55,7 +55,7 @@ class ReportingConfigForm extends React.Component { * @param {FormData} formData * @param {[String]} requiredFields */ - validateReportingForm = (formData, requiredFields) => { + validateReportingForm = (config, formData, requiredFields) => { const invalidFields = requiredFields .filter(field => !formData.get(field)) .reduce((prevFields, currField) => ({ ...prevFields, [currField]: true }), {}); @@ -63,7 +63,7 @@ class ReportingConfigForm extends React.Component { // Password is conditionally required only when pgp key will not be present // and delivery method is email if (!formData.get('pgpEncryptionKey') && formData.get('deliveryMethod') === 'email') { - if (!formData.get('encryptedPassword')) { + if (!formData.get('encryptedPassword') && !config?.encryptedPassword) { invalidFields.encryptedPassword = true; } } @@ -131,7 +131,7 @@ class ReportingConfigForm extends React.Component { requiredFields = config ? [...REQUIRED_SFTP_FIELDS] : [...REQUIRED_NEW_SFTP_FEILDS]; } // validate the form - const invalidFields = this.validateReportingForm(formData, requiredFields); + const invalidFields = this.validateReportingForm(config, formData, requiredFields); // if there are invalid fields, reflect that in the UI if (!isEmpty(invalidFields)) { this.setState((state) => ({ diff --git a/src/components/ReportingConfig/ReportingConfigForm.test.jsx b/src/components/ReportingConfig/ReportingConfigForm.test.jsx index 1e29207089..0263df7fa0 100644 --- a/src/components/ReportingConfig/ReportingConfigForm.test.jsx +++ b/src/components/ReportingConfig/ReportingConfigForm.test.jsx @@ -384,6 +384,38 @@ describe('', () => { wrapper.find('.form-control').forEach(input => input.simulate('blur')); expect(wrapper.find('input#encryptedPassword').hasClass('is-invalid')).toBeTruthy(); }); + it('Password is not required while updating if it is already present and delivery method is email', async () => { + const updateConfigMock = jest.fn().mockResolvedValue(); + const initialConfig = { + ...defaultConfig, + }; + initialConfig.encryptedPassword = 'some_pass'; + initialConfig.pgpEncryptionKey = ''; + initialConfig.hourOfDay = 4; + const requiredFields = ['hourOfDay', 'emailRaw']; + const wrapper = mount( + , + ); + + const updatedFormData = new FormData(); + updatedFormData.append('deliveryMethod', 'email'); + updatedFormData.append('pgpEncryptionKey', ''); + updatedFormData.append('encryptedPassword', ''); + + const invalidFields = await wrapper.instance().validateReportingForm( + initialConfig, + updatedFormData, + requiredFields, + ); + expect('encryptedPassword' in invalidFields).toBe(false); + }); it('Submit enterprise uuid upon report config creation', async () => { const wrapper = mount(( Date: Thu, 13 Jul 2023 15:11:24 -0600 Subject: [PATCH 006/124] fix: updating deprecated paragon components (#1007) * fix: removing deprecated components * fix: reverting the changes to form * fix: test file fixes --- .../Admin/__snapshots__/Admin.test.jsx.snap | 48 +++--- src/components/Admin/index.jsx | 30 ++-- .../CourseSearchResults.jsx | 25 +-- .../CourseSearchResults.test.jsx | 53 +++---- .../CodeSearchResults.test.jsx.snap | 91 ++++++----- src/components/CodeSearchResults/index.jsx | 29 ++-- .../CompletedLearnersTable.test.jsx.snap | 41 ++--- src/components/Coupon/Coupon.test.jsx | 142 +++++------------- src/components/CouponDetails/index.jsx | 41 ++--- ...rnersForInactiveCoursesTable.test.jsx.snap | 41 ++--- .../EnrolledLearnersTable.test.jsx.snap | 41 ++--- .../EnrollmentsTable.test.jsx.snap | 41 ++--- .../LearnerActivityTable.test.jsx.snap | 41 ++--- .../PlotlyAnalytics/PlotlyAnalyticsPage.jsx | 16 +- .../ReduxFormCheckbox.test.jsx.snap | 38 +++-- src/components/ReduxFormCheckbox/index.jsx | 26 ++-- .../RegisteredLearnersTable.test.jsx.snap | 41 ++--- .../RequestCodesPage/RequestCodesForm.jsx | 17 +-- src/components/StatusAlert/_StatusAlert.scss | 12 -- src/components/StatusAlert/index.jsx | 66 -------- src/components/TableComponent/index.jsx | 29 ++-- .../CouponDetails/CouponDetails.test.jsx | 22 +-- src/index.scss | 1 - 23 files changed, 427 insertions(+), 505 deletions(-) delete mode 100644 src/components/StatusAlert/_StatusAlert.scss delete mode 100644 src/components/StatusAlert/index.jsx diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap index 082fe5233c..13fb8a3fa2 100644 --- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap +++ b/src/components/Admin/__snapshots__/Admin.test.jsx.snap @@ -6332,35 +6332,43 @@ exports[` renders correctly with error state 1`] = ` className="col" > diff --git a/src/components/Admin/index.jsx b/src/components/Admin/index.jsx index a1d9ef7b85..4f95e3e94a 100644 --- a/src/components/Admin/index.jsx +++ b/src/components/Admin/index.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import Helmet from 'react-helmet'; -import { Icon } from '@edx/paragon'; +import { Alert, Icon } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; import { Link } from 'react-router-dom'; import Hero from '../Hero'; -import StatusAlert from '../StatusAlert'; import EnrollmentsTable from '../EnrollmentsTable'; import RegisteredLearnersTable from '../RegisteredLearnersTable'; import EnrolledLearnersTable from '../EnrolledLearnersTable'; @@ -250,24 +250,26 @@ class Admin extends React.Component { renderErrorMessage() { return ( - + + Hey, nice to see you +

Try refreshing your screen {this.props.error.message}

+
); } renderCsvErrorMessage(message) { return ( - + icon={Error} + > + Unable to generate CSV report +

Please try again. {message}

+ ); } diff --git a/src/components/BulkEnrollmentPage/CourseSearchResults.jsx b/src/components/BulkEnrollmentPage/CourseSearchResults.jsx index 673e7cbef9..970cdedc1d 100644 --- a/src/components/BulkEnrollmentPage/CourseSearchResults.jsx +++ b/src/components/BulkEnrollmentPage/CourseSearchResults.jsx @@ -5,10 +5,10 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import { connectStateResults } from 'react-instantsearch-dom'; -import { DataTable, Skeleton } from '@edx/paragon'; +import { Alert, DataTable, Skeleton } from '@edx/paragon'; +import { Error, ErrorOutline } from '@edx/paragon/icons'; import { SearchContext, SearchPagination } from '@edx/frontend-enterprise-catalog-search'; -import StatusAlert from '../StatusAlert'; import { CourseNameCell, FormattedDateCell } from './table/CourseSearchResultsCells'; import { BulkEnrollContext } from './BulkEnrollmentContext'; @@ -136,20 +136,21 @@ export const BaseCourseSearchResults = (props) => { if (!isSearchStalled && error) { return ( - + +

{ERROR_MESSAGE} {error.message}

+
); } if (!isSearchStalled && searchResults?.nbHits === 0) { return ( - +

{NO_DATA_MESSAGE}

+
); } diff --git a/src/components/BulkEnrollmentPage/CourseSearchResults.test.jsx b/src/components/BulkEnrollmentPage/CourseSearchResults.test.jsx index c33376dac5..dd69617aa0 100644 --- a/src/components/BulkEnrollmentPage/CourseSearchResults.test.jsx +++ b/src/components/BulkEnrollmentPage/CourseSearchResults.test.jsx @@ -1,22 +1,20 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { mount } from 'enzyme'; -import { screen, waitFor, within } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; import configureMockStore from 'redux-mock-store'; import userEvent from '@testing-library/user-event'; import thunk from 'redux-thunk'; -import { SearchContext, SearchPagination } from '@edx/frontend-enterprise-catalog-search'; -import { Skeleton } from '@edx/paragon'; +import { + render, screen, waitFor, within, +} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { SearchContext } from '@edx/frontend-enterprise-catalog-search'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import StatusAlert from '../StatusAlert'; import BulkEnrollContextProvider from './BulkEnrollmentContext'; import { BaseCourseSearchResults, NO_DATA_MESSAGE, TABLE_HEADERS, } from './CourseSearchResults'; import { renderWithRouter } from '../test/testUtils'; - import '../../../__mocks__/react-instantsearch-dom'; const mockStore = configureMockStore([thunk]); @@ -97,21 +95,15 @@ const CourseSearchWrapper = ({ value = { refinements }, props = defaultProps }) describe('', () => { it('renders search results', () => { - const wrapper = mount(); - - // 5 header columns: selection, Course name, Partner, Course Date, and enrollment - const tableHeaderCells = wrapper.find('TableHeaderCell'); - expect(tableHeaderCells.length).toBe(4); - expect(tableHeaderCells.at(1).prop('Header')).toBe(TABLE_HEADERS.courseName); - expect(tableHeaderCells.at(2).prop('Header')).toBe(TABLE_HEADERS.partnerName); - expect(tableHeaderCells.at(3).prop('Header')).toBe(TABLE_HEADERS.courseAvailability); + render(); + screen.getByRole('columnheader', { name: TABLE_HEADERS.courseName }); + screen.getByRole('columnheader', { name: TABLE_HEADERS.partnerName }); + screen.getByRole('columnheader', { name: TABLE_HEADERS.courseAvailability }); - // 5 table cells: selection, course name, partner, start date, and enrollment - const tableCells = wrapper.find('TableCell'); - expect(tableCells.length).toBe(8); // 2 rows x 4 columns - expect(tableCells.at(1).text()).toBe(testCourseName); - expect(tableCells.at(2).text()).toBe('edX'); - expect(tableCells.at(3).text()).toBe('Sep 10, 2020 - Sep 10, 2030'); + expect(screen.getAllByRole('cell')).toHaveLength(8); + screen.getByRole('cell', { name: testCourseName }); + screen.getByRole('cell', { name: 'Sep 10, 2020 - Sep 10, 2030' }); + expect(screen.getAllByRole('cell', { name: 'edX' })).toHaveLength(2); }); it('renders popover with course description', async () => { renderWithRouter(); @@ -123,17 +115,19 @@ describe('', () => { }); }); it('displays search pagination', () => { - const wrapper = mount(); - expect(wrapper.find(SearchPagination)).toHaveLength(1); + renderWithRouter(); + expect(screen.getByText('Navigate Right')); + expect(screen.getByText('Navigate Left')); }); it('returns an error message if there\'s an error', () => { const errorMsg = 'It did not work'; - const wrapper = mount(); - expect(wrapper.text()).toContain(errorMsg); + const expectedError = `An error occured while retrieving data ${errorMsg}`; + renderWithRouter(); + expect(screen.getByText(expectedError)); }); it('renders a loading state when loading algolia results', () => { - const wrapper = mount(); - expect(wrapper.find(Skeleton)).toHaveLength(1); + renderWithRouter(); + expect(screen.getByText('Loading...')); }); it('shows selection options when at least one course is selected', () => { renderWithRouter(); @@ -142,10 +136,9 @@ describe('', () => { expect(screen.getByText('1 selected (1 shown below)', { exact: false })).toBeInTheDocument(); }); it('renders a message when there are no results', () => { - const wrapper = mount(); - expect(wrapper.find(StatusAlert)).toHaveLength(1); - expect(wrapper.text()).toContain(NO_DATA_MESSAGE); + expect(screen.getByText(NO_DATA_MESSAGE)); }); }); diff --git a/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap b/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap index d411e7d841..8395dc41f8 100644 --- a/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap +++ b/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap @@ -55,30 +55,35 @@ exports[` basic rendering should render empty table data 1` @@ -135,42 +140,50 @@ exports[` basic rendering should render error 1`] = ` Close search results diff --git a/src/components/CodeSearchResults/index.jsx b/src/components/CodeSearchResults/index.jsx index 8dbee143c4..d6fe3ebea3 100644 --- a/src/components/CodeSearchResults/index.jsx +++ b/src/components/CodeSearchResults/index.jsx @@ -1,10 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { TransitionReplace } from '@edx/paragon'; +import { Alert, TransitionReplace } from '@edx/paragon'; +import { CheckCircle } from '@edx/paragon/icons'; import { updateUrl } from '../../utils'; -import StatusAlert from '../StatusAlert'; import CodeSearchResultsHeading from './CodeSearchResultsHeading'; import CodeSearchResultsTable from './CodeSearchResultsTable'; @@ -57,14 +57,15 @@ class CodeSearchResults extends React.Component { }); }; - renderSuccessMessage = options => ( - ( + + > +

{message}

+
); render() { @@ -82,12 +83,12 @@ class CodeSearchResults extends React.Component { searchQuery={searchQuery} onClose={onClose} /> - {isCodeReminderSuccessful && this.renderSuccessMessage({ - message: `A reminder was successfully sent to ${searchQuery}.`, - })} - {isCodeRevokeSuccessful && this.renderSuccessMessage({ - message: 'Successfully revoked code(s)', - })} + {isCodeReminderSuccessful && this.renderSuccessMessage( + `A reminder was successfully sent to ${searchQuery}.`, + )} + {isCodeRevokeSuccessful && this.renderSuccessMessage( + 'Successfully revoked code(s)', + )}
+ className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # cou

- - Show details + + + + + Show details +
@@ -312,10 +340,24 @@ exports[` renders correctly with dashboard analytics data renders # cou 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # cou

- - Show details + + + + + Show details +
@@ -421,10 +477,24 @@ exports[` renders correctly with dashboard analytics data renders # cou 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # cou

- - Show details + + + + + Show details +
@@ -546,10 +630,24 @@ exports[` renders correctly with dashboard analytics data renders # cou 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # cou

- - Show details + + + + + Show details +
@@ -682,10 +794,24 @@ exports[` renders correctly with dashboard analytics data renders # cou onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -724,11 +850,19 @@ exports[` renders correctly with dashboard analytics data renders # cou onClick={[Function]} type="button" > - + + + Download current report (CSV) @@ -798,10 +932,24 @@ exports[` renders correctly with dashboard analytics data renders # of 3 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -891,10 +1053,24 @@ exports[` renders correctly with dashboard analytics data renders # of 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -1000,10 +1190,24 @@ exports[` renders correctly with dashboard analytics data renders # of 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -1125,10 +1343,24 @@ exports[` renders correctly with dashboard analytics data renders # of 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -1261,10 +1507,24 @@ exports[` renders correctly with dashboard analytics data renders # of onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -1303,11 +1563,19 @@ exports[` renders correctly with dashboard analytics data renders # of onClick={[Function]} type="button" > - + + + Download current report (CSV) @@ -1377,10 +1645,24 @@ exports[` renders correctly with dashboard analytics data renders # of 3 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -1470,10 +1766,24 @@ exports[` renders correctly with dashboard analytics data renders # of 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -1579,10 +1903,24 @@ exports[` renders correctly with dashboard analytics data renders # of 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -1704,10 +2056,24 @@ exports[` renders correctly with dashboard analytics data renders # of 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders # of

- - Show details + + + + + Show details +
@@ -1840,10 +2220,24 @@ exports[` renders correctly with dashboard analytics data renders # of onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -1886,11 +2280,19 @@ exports[` renders correctly with dashboard analytics data renders # of onClick={[Function]} type="button" > - + + + Download current report (CSV) @@ -1960,10 +2362,24 @@ exports[` renders correctly with dashboard analytics data renders colla 3 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders colla

- - Show details + + + + + Show details +
@@ -2053,10 +2483,24 @@ exports[` renders correctly with dashboard analytics data renders colla 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders colla

- - Show details + + + + + Show details +
@@ -2162,10 +2620,24 @@ exports[` renders correctly with dashboard analytics data renders colla 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders colla

- - Show details + + + + + Show details +
@@ -2287,10 +2773,24 @@ exports[` renders correctly with dashboard analytics data renders colla 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders colla

- - Show details + + + + + Show details +
@@ -2451,11 +2965,19 @@ exports[` renders correctly with dashboard analytics data renders colla onClick={[Function]} type="button" > - + + + Download full report (CSV) @@ -2477,7 +2999,7 @@ exports[` renders correctly with dashboard analytics data renders colla > @@ -2486,7 +3008,7 @@ exports[` renders correctly with dashboard analytics data renders colla > renders correctly with dashboard analytics data renders colla > + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders full

- - Show details + + + + + Show details +
@@ -2900,10 +3480,24 @@ exports[` renders correctly with dashboard analytics data renders full 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders full

- - Show details + + + + + Show details +
@@ -3025,10 +3633,24 @@ exports[` renders correctly with dashboard analytics data renders full 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders full

- - Show details + + + + + Show details +
@@ -3189,11 +3825,19 @@ exports[` renders correctly with dashboard analytics data renders full onClick={[Function]} type="button" > - + + + Download full report (CSV) @@ -3215,7 +3859,7 @@ exports[` renders correctly with dashboard analytics data renders full > @@ -3224,7 +3868,7 @@ exports[` renders correctly with dashboard analytics data renders full > renders correctly with dashboard analytics data renders full > + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders inact

- - Show details + + + + + Show details +
@@ -3638,10 +4340,24 @@ exports[` renders correctly with dashboard analytics data renders inact 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders inact

- - Show details + + + + + Show details +
@@ -3763,10 +4493,24 @@ exports[` renders correctly with dashboard analytics data renders inact 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders inact

- - Show details + + + + + Show details +
@@ -3899,10 +4657,24 @@ exports[` renders correctly with dashboard analytics data renders inact onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -3945,11 +4717,19 @@ exports[` renders correctly with dashboard analytics data renders inact onClick={[Function]} type="button" > - + + + Download current report (CSV) @@ -4019,10 +4799,24 @@ exports[` renders correctly with dashboard analytics data renders inact 3 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders inact

- - Show details + + + + + Show details +
@@ -4112,10 +4920,24 @@ exports[` renders correctly with dashboard analytics data renders inact 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders inact

- - Show details + + + + + Show details +
@@ -4221,10 +5057,24 @@ exports[` renders correctly with dashboard analytics data renders inact 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders inact

- - Show details + + + + + Show details +
@@ -4346,10 +5210,24 @@ exports[` renders correctly with dashboard analytics data renders inact 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders inact

- - Show details + + + + + Show details +
@@ -4482,10 +5374,24 @@ exports[` renders correctly with dashboard analytics data renders inact onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -4528,11 +5434,19 @@ exports[` renders correctly with dashboard analytics data renders inact onClick={[Function]} type="button" > - + + + Download current report (CSV) @@ -4602,10 +5516,24 @@ exports[` renders correctly with dashboard analytics data renders learn 3 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders learn

- - Show details + + + + + Show details +
@@ -4695,10 +5637,24 @@ exports[` renders correctly with dashboard analytics data renders learn 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders learn

- - Show details + + + + + Show details +
@@ -4804,10 +5774,24 @@ exports[` renders correctly with dashboard analytics data renders learn 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders learn

- - Show details + + + + + Show details +
@@ -4929,10 +5927,24 @@ exports[` renders correctly with dashboard analytics data renders learn 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders learn

- - Show details + + + + + Show details +
@@ -5065,10 +6091,24 @@ exports[` renders correctly with dashboard analytics data renders learn onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -5111,11 +6151,19 @@ exports[` renders correctly with dashboard analytics data renders learn onClick={[Function]} type="button" > - + + + Download current report (CSV) @@ -5185,10 +6233,24 @@ exports[` renders correctly with dashboard analytics data renders regis 3 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders regis

- - Show details + + + + + Show details +
@@ -5278,10 +6354,24 @@ exports[` renders correctly with dashboard analytics data renders regis 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders regis

- - Show details + + + + + Show details +
@@ -5387,10 +6491,24 @@ exports[` renders correctly with dashboard analytics data renders regis 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders regis

- - Show details + + + + + Show details +
@@ -5512,10 +6644,24 @@ exports[` renders correctly with dashboard analytics data renders regis 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders regis

- - Show details + + + + + Show details +
@@ -5648,10 +6808,24 @@ exports[` renders correctly with dashboard analytics data renders regis onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -5690,11 +6864,19 @@ exports[` renders correctly with dashboard analytics data renders regis onClick={[Function]} type="button" > - + + + Download current report (CSV) @@ -5764,10 +6946,24 @@ exports[` renders correctly with dashboard analytics data renders top a 3 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders top a

- - Show details + + + + + Show details +
@@ -5857,10 +7067,24 @@ exports[` renders correctly with dashboard analytics data renders top a 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders top a

- - Show details + + + + + Show details +
@@ -5966,10 +7204,24 @@ exports[` renders correctly with dashboard analytics data renders top a 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders top a

- - Show details + + + + + Show details +
@@ -6091,10 +7357,24 @@ exports[` renders correctly with dashboard analytics data renders top a 1 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with dashboard analytics data renders top a

- - Show details + + + + + Show details +
@@ -6227,10 +7521,24 @@ exports[` renders correctly with dashboard analytics data renders top a onClick={[Function]} > + className="pgn__icon mr-2" + > + + + + Reset to Full Report @@ -6273,11 +7581,19 @@ exports[` renders correctly with dashboard analytics data renders top a onClick={[Function]} type="button" > - + + + Download current report (CSV) diff --git a/src/components/Admin/index.jsx b/src/components/Admin/index.jsx index 4f95e3e94a..45db4b5059 100644 --- a/src/components/Admin/index.jsx +++ b/src/components/Admin/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Helmet from 'react-helmet'; import { Alert, Icon } from '@edx/paragon'; -import { Error } from '@edx/paragon/icons'; +import { Error, Undo } from '@edx/paragon/icons'; import { Link } from 'react-router-dom'; import Hero from '../Hero'; @@ -225,7 +225,7 @@ class Admin extends React.Component { return ( - + Reset to {this.getMetadataForAction().title} ); @@ -242,7 +242,7 @@ class Admin extends React.Component { const resetLink = resetQuery ? `${pathname}?${resetQuery}` : pathname; return ( - + Reset Filters ); diff --git a/src/components/CodeAssignmentModal/constants.jsx b/src/components/CodeAssignmentModal/constants.jsx index 995adefa65..a1f427ebd1 100644 --- a/src/components/CodeAssignmentModal/constants.jsx +++ b/src/components/CodeAssignmentModal/constants.jsx @@ -1,4 +1,4 @@ -import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import { Info } from '@edx/paragon/icons'; import { MODAL_TYPES } from '../EmailTemplateForm/constants'; import { getTemplateEmailFields } from '../EmailTemplateForm'; import CheckboxWithTooltip from '../ReduxFormCheckbox/CheckboxWithTooltip'; @@ -20,7 +20,7 @@ export const getAssignmentModalFields = formatMessage => { id: EMAIL_TEMPLATE_NUDGE_EMAIL_ID, component: CheckboxWithTooltip, className: 'auto-reminder-wrapper', - icon: faInfoCircle, + icon: Info, altText: formatMessage(messages.modalAltText), tooltipText: formatMessage(messages.modalTooltipText), label: formatMessage(messages.modalFieldLabel), diff --git a/src/components/CodeAssignmentModal/index.jsx b/src/components/CodeAssignmentModal/index.jsx index e492fef0b2..270411d242 100644 --- a/src/components/CodeAssignmentModal/index.jsx +++ b/src/components/CodeAssignmentModal/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { reduxForm, SubmissionError } from 'redux-form'; import { - Button, Icon, Modal, Form, + Button, Modal, Form, Spinner, } from '@edx/paragon'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -418,7 +418,7 @@ export class BaseCodeAssignmentModal extends React.Component { data-testid={SUBMIT_BUTTON_TEST_ID} > <> - {mode === MODAL_TYPES.assign && submitting && } + {mode === MODAL_TYPES.assign && submitting && } {`Assign ${isBulkAssign ? 'Codes' : 'Code'}`} , diff --git a/src/components/CodeManagement/ManageCodesTab.jsx b/src/components/CodeManagement/ManageCodesTab.jsx index 23f4dd7195..1e7e3cb61e 100644 --- a/src/components/CodeManagement/ManageCodesTab.jsx +++ b/src/components/CodeManagement/ManageCodesTab.jsx @@ -9,7 +9,9 @@ import { Icon, Pagination, } from '@edx/paragon'; -import { CheckCircle, Info, WarningFilled } from '@edx/paragon/icons'; +import { + CheckCircle, Info, Plus, SpinnerIcon, WarningFilled, +} from '@edx/paragon/icons'; import SearchBar from '../SearchBar'; import CodeSearchResults from '../CodeSearchResults'; @@ -277,7 +279,7 @@ class ManageCodesTab extends React.Component { disabled={loading} > <> - + Refresh data @@ -286,7 +288,7 @@ class ManageCodesTab extends React.Component { to={`/${enterpriseSlug}/admin/${ROUTE_NAMES.codeManagement}/request-codes`} > <> - + Request more codes diff --git a/src/components/CodeManagement/tests/ManageCodesTab.test.jsx b/src/components/CodeManagement/tests/ManageCodesTab.test.jsx index ea08c4cf96..2e8451de7a 100644 --- a/src/components/CodeManagement/tests/ManageCodesTab.test.jsx +++ b/src/components/CodeManagement/tests/ManageCodesTab.test.jsx @@ -268,7 +268,7 @@ describe('ManageCodesTabWrapper', () => { const store = mockStore({ ...initialState }); const wrapper = mount(); store.clearActions(); - wrapper.find('.fa-refresh').hostNodes().simulate('click'); + wrapper.find('[data-testid="refresh-data"]').hostNodes().simulate('click'); expect(store.getActions().filter(action => action.type === COUPONS_REQUEST)).toHaveLength(1); }); diff --git a/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap b/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap index 7e568a0fdc..13de3d3b8a 100644 --- a/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap +++ b/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap @@ -91,10 +91,25 @@ Array [ type="button" > + className="pgn__icon mr-2" + data-testid="refresh-data" + > + + + + Refresh data + className="pgn__icon" + > + + + + Request more codes @@ -201,7 +230,7 @@ Array [ >
)} > - +
); }; IconWithTooltip.propTypes = { - icon: PropTypes.shape({}).isRequired, + icon: PropTypes.func.isRequired, altText: PropTypes.string.isRequired, tooltipText: PropTypes.string.isRequired, // These props have defaults above diff --git a/src/components/InviteLearnersModal/index.jsx b/src/components/InviteLearnersModal/index.jsx index 1def540adb..89ee1e62d6 100644 --- a/src/components/InviteLearnersModal/index.jsx +++ b/src/components/InviteLearnersModal/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Field, reduxForm, SubmissionError } from 'redux-form'; import { - Button, Icon, Modal, Alert, + Button, Modal, Alert, Spinner, } from '@edx/paragon'; import { Cancel as ErrorIcon } from '@edx/paragon/icons'; @@ -192,7 +192,7 @@ class InviteLearnersModal extends React.Component { onClick={handleSubmit(this.handleModalSubmit)} > <> - {submitting && } + {submitting && } Invite learners , diff --git a/src/components/MultipleFileInputField/MultipleFileInputField.jsx b/src/components/MultipleFileInputField/MultipleFileInputField.jsx index 86363bbaca..5e3419bfce 100644 --- a/src/components/MultipleFileInputField/MultipleFileInputField.jsx +++ b/src/components/MultipleFileInputField/MultipleFileInputField.jsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { Form, FormControl, IconButton, } from '@edx/paragon'; +import { Close } from '@edx/paragon/icons'; import classNames from 'classnames'; -import * as FontAwesome from '@fortawesome/free-solid-svg-icons/faTimes'; import { getSizeInBytes, formatBytes } from './utils'; import { MAX_FILES_SIZE, FILE_SIZE_EXCEEDS_ERROR } from './constants'; @@ -91,7 +91,7 @@ const MultipleFileInputField = ({ inputValues?.map((e, i) => (
{`${e.name} - ${formatBytes(e.size)}`} - handleFileRemoveClick(i)} variant="danger" /> + handleFileRemoveClick(i)} variant="danger" />
)) } diff --git a/src/components/NumberCard/NumberCard.test.jsx b/src/components/NumberCard/NumberCard.test.jsx index 042694244d..fff59a6746 100644 --- a/src/components/NumberCard/NumberCard.test.jsx +++ b/src/components/NumberCard/NumberCard.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; +import { Groups } from '@edx/paragon/icons'; import NumberCard from './index'; @@ -14,7 +15,7 @@ const NumberCardWrapper = props => ( className="test-class" title={10} description="This describes the data!" - iconClassName="fa fa-users" + icon={Groups} {...props} /> @@ -134,10 +135,9 @@ describe('', () => { }]} /> )); - wrapper.setProps({ detailsExpanded: true }); const action = getNumberCard(wrapper).find('.footer-body .btn-link').hostNodes().first(); - expect(action.find('.fa-spinner').exists()).toBeTruthy(); + expect(action.find('.ml-2').exists()).toBeTruthy(); }); }); }); diff --git a/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap b/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap index 59b61ff6c0..415a2c1b3e 100644 --- a/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap +++ b/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap @@ -17,10 +17,24 @@ exports[` renders correctly with detail actions 1`] = ` 10 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

renders correctly with detail actions 1`] = `

- - Show details + + + + + Show details +
@@ -125,10 +153,24 @@ exports[` renders correctly without detail actions 1`] = ` 10 + className="pgn__icon d-flex align-items-center justify-content-center" + > + + + +

{action.loading - && } + && } )); @@ -161,7 +162,7 @@ class NumberCard extends React.Component { const { className, title, - iconClassName, + icon, description, detailActions, id, @@ -180,14 +181,14 @@ class NumberCard extends React.Component { {this.formatTitle(title)} - {iconClassName && ( + {icon && ( )} @@ -212,13 +213,7 @@ class NumberCard extends React.Component {

@@ -246,7 +241,7 @@ class NumberCard extends React.Component { NumberCard.defaultProps = { className: null, - iconClassName: null, + icon: null, detailActions: null, detailsExpanded: false, }; @@ -256,7 +251,7 @@ NumberCard.propTypes = { description: PropTypes.string.isRequired, id: PropTypes.string.isRequired, className: PropTypes.string, - iconClassName: PropTypes.string, + icon: PropTypes.func, detailActions: PropTypes.arrayOf(PropTypes.shape({ label: PropTypes.string, loading: PropTypes.bool, diff --git a/src/components/ReduxFormCheckbox/CheckboxWithTooltip.jsx b/src/components/ReduxFormCheckbox/CheckboxWithTooltip.jsx index be577a2cfa..1473d8a47c 100644 --- a/src/components/ReduxFormCheckbox/CheckboxWithTooltip.jsx +++ b/src/components/ReduxFormCheckbox/CheckboxWithTooltip.jsx @@ -26,7 +26,7 @@ CheckboxWithTooltip.defaultProps = { CheckboxWithTooltip.propTypes = { className: PropTypes.string, // Icon should be a paragon icon - icon: PropTypes.shape().isRequired, + icon: PropTypes.func.isRequired, altText: PropTypes.string.isRequired, tooltipText: PropTypes.string.isRequired, }; diff --git a/src/components/ReportingConfig/ReportingConfigForm.jsx b/src/components/ReportingConfig/ReportingConfigForm.jsx index 4e662c6a11..09dadbe4e5 100644 --- a/src/components/ReportingConfig/ReportingConfigForm.jsx +++ b/src/components/ReportingConfig/ReportingConfigForm.jsx @@ -3,8 +3,9 @@ import PropTypes from 'prop-types'; import isEmpty from 'lodash/isEmpty'; import omit from 'lodash/omit'; import { - ValidationFormGroup, Input, StatefulButton, Icon, Button, + ValidationFormGroup, Input, StatefulButton, Icon, Button, Spinner, } from '@edx/paragon'; +import { Check, Close, Download } from '@edx/paragon/icons'; import { camelCaseObject } from '@edx/frontend-platform/utils'; import SFTPDeliveryMethodForm from './SFTPDeliveryMethodForm'; import EmailDeliveryMethodForm from './EmailDeliveryMethodForm'; @@ -419,10 +420,10 @@ class ReportingConfigForm extends React.Component { error: 'Error', }} icons={{ - default: , - pending: , - complete: , - error: , + default: , + pending: , + complete: , + error: , }} disabledStates={[SUBMIT_STATES.PENDING]} className="ml-3 col" @@ -434,7 +435,7 @@ class ReportingConfigForm extends React.Component { className="btn-outline-danger mr-3" onClick={() => this.props.deleteConfig(config.uuid)} > - Delete + Delete )}
diff --git a/src/components/ReportingConfig/index.jsx b/src/components/ReportingConfig/index.jsx index e34c1f17e3..f77748e903 100644 --- a/src/components/ReportingConfig/index.jsx +++ b/src/components/ReportingConfig/index.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Collapsible, Icon } from '@edx/paragon'; +import { Check, Close } from '@edx/paragon/icons'; import { camelCaseObject } from '@edx/frontend-platform'; import EnterpriseCatalogApiService from '../../data/services/EnterpriseCatalogApiService'; import LMSApiService from '../../data/services/LmsApiService'; @@ -142,9 +143,17 @@ class ReportingConfig extends React.Component { className="shadow" title={(
- + {config.active ? ( + + ) : ( + + )}

Report Type:

{config.data_type}

diff --git a/src/components/RequestCodesPage/RequestCodesForm.jsx b/src/components/RequestCodesPage/RequestCodesForm.jsx index 50fdb226ce..fd46d99187 100644 --- a/src/components/RequestCodesPage/RequestCodesForm.jsx +++ b/src/components/RequestCodesPage/RequestCodesForm.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Field, reduxForm } from 'redux-form'; import { Link, Redirect } from 'react-router-dom'; -import { Alert, Button, Icon } from '@edx/paragon'; +import { Alert, Button, Spinner } from '@edx/paragon'; import RenderField from '../RenderField'; @@ -120,7 +120,7 @@ class RequestCodesForm extends React.Component { className="btn-primary" > <> - {submitting && } + {submitting && } Request Codes diff --git a/src/components/SaveTemplateButton/index.jsx b/src/components/SaveTemplateButton/index.jsx index afe95f763c..efcf854b81 100644 --- a/src/components/SaveTemplateButton/index.jsx +++ b/src/components/SaveTemplateButton/index.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { StatefulButton, Icon } from '@edx/paragon'; +import { StatefulButton, Icon, Spinner } from '@edx/paragon'; +import { CheckCircle } from '@edx/paragon/icons'; import { SubmissionError } from 'redux-form'; import { validateEmailTemplateFields } from '../../data/validation/email'; @@ -149,8 +150,8 @@ class SaveTemplateButton extends React.Component { complete: 'Template Saved', }} icons={{ - pending: , - complete: , + pending: , + complete: , }} disabledStates={[SUBMIT_STATES.PENDING]} disabled={disabled} diff --git a/src/components/Sidebar/IconLink.test.jsx b/src/components/Sidebar/IconLink.test.jsx index 5108123617..e34f9aaf2a 100644 --- a/src/components/Sidebar/IconLink.test.jsx +++ b/src/components/Sidebar/IconLink.test.jsx @@ -3,8 +3,8 @@ import { } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faUniversity } from '@fortawesome/free-solid-svg-icons'; +import { Icon } from '@edx/paragon'; +import { School } from '@edx/paragon/icons'; import { MemoryRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; @@ -28,7 +28,7 @@ describe('', () => { const defaultProps = { title: 'Internal Route', to: 'admin/test', - icon: , + icon: , }; render(); expect(screen.getByText('Internal Route').closest('a')).toHaveAttribute('href', '/admin/test'); @@ -38,7 +38,7 @@ describe('', () => { const defaultProps = { title: 'External Route', to: 'http://helpcenter.edx.org/us', - icon: , + icon: , external: true, }; diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index f0515898c5..5a78bf11e8 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -3,13 +3,10 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { faFile, faLifeRing } from '@fortawesome/free-regular-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - faCreditCard, faTags, faChartLine, faChartBar, faCog, -} from '@fortawesome/free-solid-svg-icons'; import { Icon } from '@edx/paragon'; -import { MoneyOutline, BookOpen } from '@edx/paragon/icons'; +import { + BookOpen, CreditCard, Description, InsertChartOutlined, MoneyOutline, Settings, Support, Tag, TrendingUp, +} from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import IconLink from './IconLink'; @@ -73,25 +70,25 @@ const Sidebar = ({ { title: 'Learner Progress Report', to: `${baseUrl}/admin/${ROUTE_NAMES.learners}`, - icon: , + icon: , }, { title: 'Analytics', to: `${baseUrl}/admin/${ROUTE_NAMES.analytics}`, - icon: , + icon: , hidden: !features.ANALYTICS || !enableAnalyticsScreen, }, { title: 'Code Management', to: `${baseUrl}/admin/${ROUTE_NAMES.codeManagement}`, - icon: , + icon: , hidden: !features.CODE_MANAGEMENT || !enableCodeManagementScreen, notification: !!subsidyRequestsCounts.couponCodes, }, { title: 'Subscription Management', to: `${baseUrl}/admin/${ROUTE_NAMES.subscriptionManagement}`, - icon: , + icon: , hidden: !enableSubscriptionManagementScreen, notification: !!subsidyRequestsCounts.subscriptionLicenses, }, @@ -112,20 +109,20 @@ const Sidebar = ({ { title: 'Reporting Configurations', to: `${baseUrl}/admin/${ROUTE_NAMES.reporting}`, - icon: , + icon: , hidden: !features.REPORTING_CONFIGURATIONS || !enableReportingConfigScreen, }, { title: 'Settings', id: TOUR_TARGETS.SETTINGS_SIDEBAR, to: `${baseUrl}/admin/${ROUTE_NAMES.settings}`, - icon: , + icon: , }, // NOTE: keep "Support" link the last nav item { title: 'Support', to: configuration.ENTERPRISE_SUPPORT_URL, - icon: , + icon: , hidden: !features.SUPPORT, external: true, }, diff --git a/src/components/subscriptions/SubscriptionDetails.jsx b/src/components/subscriptions/SubscriptionDetails.jsx index 3ef662d0c2..a41fd63e2d 100644 --- a/src/components/subscriptions/SubscriptionDetails.jsx +++ b/src/components/subscriptions/SubscriptionDetails.jsx @@ -3,11 +3,10 @@ import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import dayjs from 'dayjs'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faAngleLeft } from '@fortawesome/free-solid-svg-icons'; import { - Button, Row, Col, Toast, + Button, Row, Col, Toast, Icon, } from '@edx/paragon'; +import { ArrowBackIos } from '@edx/paragon/icons'; import { SubscriptionDetailContext } from './SubscriptionDetailContextProvider'; import InviteLearnersButton from './buttons/InviteLearnersButton'; @@ -38,7 +37,7 @@ const SubscriptionDetails = ({ enterpriseSlug }) => { diff --git a/src/containers/CouponDetails/CouponDetails.test.jsx b/src/containers/CouponDetails/CouponDetails.test.jsx index 0499565dbb..0c6038c94a 100644 --- a/src/containers/CouponDetails/CouponDetails.test.jsx +++ b/src/containers/CouponDetails/CouponDetails.test.jsx @@ -163,7 +163,7 @@ describe('CouponDetails container', () => { const selectAllCodesOnPage = ({ isSelected, expectedSelectionLength }) => { const selectAllCheckbox = wrapper.find('table th').find('input[type=\'checkbox\']'); - selectAllCheckbox.simulate('change', { target: { value: isSelected } }); + selectAllCheckbox.simulate('change', { target: { checked: isSelected } }); expect(wrapper.find('CouponDetails').instance().state.selectedCodes).toHaveLength(expectedSelectionLength); }; @@ -594,8 +594,8 @@ describe('CouponDetails container', () => { it('handles individual code selection within table', () => { const checkboxes = wrapper.find('table').find('input[type=\'checkbox\']').slice(1); - checkboxes.first().simulate('change', { target: { value: true } }); - checkboxes.last().simulate('change', { target: { value: true } }); + checkboxes.first().simulate('change', { target: { checked: true } }); + checkboxes.last().simulate('change', { target: { checked: true } }); expect(wrapper.find('CouponDetails').instance().state.selectedCodes).toHaveLength(2); diff --git a/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap b/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap index 33e4cd3f06..49c9024f3b 100644 --- a/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap +++ b/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap @@ -31,23 +31,26 @@ exports[` renders correctly 1`] = ` - + + + + renders correctly 1`] = ` - + + + + renders correctly 1`] = ` - + + + + renders correctly 1`] = ` - + + + + renders correctly when code management is hidden 1`] = ` - + + + + renders correctly when code management is hidden 1`] = ` - + + + + renders correctly when expanded 1`] = ` - + + + + Learner Progress Report @@ -448,23 +469,26 @@ exports[` renders correctly when expanded 1`] = ` - + + + + Code Management @@ -487,23 +511,26 @@ exports[` renders correctly when expanded 1`] = ` - + + + + Subscription Management @@ -569,23 +596,26 @@ exports[` renders correctly when expanded 1`] = ` - + + + + Settings @@ -629,23 +659,26 @@ exports[` renders correctly when expanded by toggle 1`] = ` - + + + + Learner Progress Report @@ -668,23 +701,26 @@ exports[` renders correctly when expanded by toggle 1`] = ` - + + + + Code Management @@ -707,23 +743,26 @@ exports[` renders correctly when expanded by toggle 1`] = ` - + + + + Subscription Management @@ -789,23 +828,26 @@ exports[` renders correctly when expanded by toggle 1`] = ` - + + + + Settings diff --git a/src/index.scss b/src/index.scss index e7f2a1fa57..34c7e99ba0 100644 --- a/src/index.scss +++ b/src/index.scss @@ -7,8 +7,6 @@ $modal-max-width: 650px; @import "~@edx/frontend-enterprise-catalog-search"; @import "~@edx/brand/paragon/overrides"; -$fa-font-path: "~font-awesome/fonts"; -@import "~font-awesome/scss/font-awesome"; @import "./components/EnterpriseApp/EnterpriseApp"; @import "./components/CodeSearchResults/CodeSearchResults"; @@ -27,7 +25,6 @@ $fa-font-path: "~font-awesome/fonts"; @import "./components/Admin/Admin"; @import "./components/settings/settings"; - body { overflow-x: hidden; } From fc3a70fc07eca5dfd6166fd37181ac32b95c4daa Mon Sep 17 00:00:00 2001 From: irfanuddinahmad <34648393+irfanuddinahmad@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:56:28 +0500 Subject: [PATCH 015/124] feat: Added support for multiple active offers in subsidies context (#1020) Co-authored-by: IrfanUddinAhmad --- .../EnterpriseSubsidiesContext/data/hooks.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index 123d022df1..d699098cd0 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -41,26 +41,26 @@ export const useEnterpriseOffers = ({ enablePortalLearnerCreditManagementScreen, let activeSubsidyFound = false; if (results.length !== 0) { let subsidy = results[0]; + const offerData = []; + let activeSubsidyData = {}; for (let i = 0; i < results.length; i++) { subsidy = results[i]; activeSubsidyFound = source === 'ecommerceApi' ? subsidy.isCurrent : subsidy.isActive; if (activeSubsidyFound === true) { - break; + activeSubsidyData = { + id: subsidy.uuid || subsidy.id, + name: subsidy.title || subsidy.displayName, + start: subsidy.activeDatetime || subsidy.startDatetime, + end: subsidy.expirationDatetime || subsidy.endDatetime, + isCurrent: activeSubsidyFound, + }; + offerData.push(activeSubsidyData); + setCanManageLearnerCredit(true); } } - if (activeSubsidyFound === true) { - const offerData = { - id: subsidy.uuid || subsidy.id, - name: subsidy.title || subsidy.displayName, - start: subsidy.activeDatetime || subsidy.startDatetime, - end: subsidy.expirationDatetime || subsidy.endDatetime, - isCurrent: activeSubsidyFound, - }; - setOffers([offerData]); - setCanManageLearnerCredit(true); - } + setOffers(offerData); } } catch (error) { logError(error); From 4266584cb92dfe79d42c84cfa433aa7d912300dc Mon Sep 17 00:00:00 2001 From: Zaman Afzal Date: Wed, 23 Aug 2023 00:35:37 +0500 Subject: [PATCH 016/124] feat: Show exec-ed learner credit only for exec ed offers (#1021) --- .../learner-credit-management/BudgetCard.jsx | 24 ++++++---- .../data/constants.js | 2 + .../data/tests/utils.test.js | 5 ++ .../learner-credit-management/data/utils.js | 2 + .../tests/BudgetCard.test.jsx | 48 ++++++++++++++++++- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/components/learner-credit-management/BudgetCard.jsx b/src/components/learner-credit-management/BudgetCard.jsx index 138113f964..247769c715 100644 --- a/src/components/learner-credit-management/BudgetCard.jsx +++ b/src/components/learner-credit-management/BudgetCard.jsx @@ -15,6 +15,7 @@ import { useOfferRedemptions, useOfferSummary } from './data/hooks'; import LearnerCreditAggregateCards from './LearnerCreditAggregateCards'; import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { EXEC_ED_OFFER_TYPE } from './data/constants'; const BudgetCard = ({ offer, @@ -136,16 +137,19 @@ const BudgetCard = ({ - - - - {renderCardHeader('Executive Education')} - {renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFundsExecEd)} - - - + {offerSummary?.offerType === EXEC_ED_OFFER_TYPE + && ( + + + + {renderCardHeader('Executive Education')} + {renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFundsExecEd)} + + + + )} ) : ( diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 6c11820c1c..2e27404dee 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -14,3 +14,5 @@ export const LOW_REMAINING_BALANCE_PERCENT_THRESHOLD = 0.75; export const NO_BALANCE_REMAINING_DOLLAR_THRESHOLD = 100; export const DATE_FORMAT = 'MMMM DD, YYYY'; + +export const EXEC_ED_OFFER_TYPE = 'learner_credit'; diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js index 88773efbcc..33902d40fe 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -1,4 +1,5 @@ import { transformOfferSummary } from '../utils'; +import { EXEC_ED_OFFER_TYPE } from '../constants'; describe('transformOfferSummary', () => { it('should return null if there is no offerSummary', () => { @@ -11,6 +12,7 @@ describe('transformOfferSummary', () => { amountOfOfferSpent: 1.34, remainingBalance: -0.34, percentOfOfferSpent: 1.34, + offerType: EXEC_ED_OFFER_TYPE, }; expect(transformOfferSummary(offerSummary)).toEqual({ @@ -20,6 +22,7 @@ describe('transformOfferSummary', () => { redeemedFundsOcm: NaN, remainingFunds: 0.0, percentUtilized: 1.0, + offerType: EXEC_ED_OFFER_TYPE, }); }); @@ -29,6 +32,7 @@ describe('transformOfferSummary', () => { amountOfOfferSpent: 100, remainingBalance: null, percentOfOfferSpent: null, + offerType: 'Site', }; expect(transformOfferSummary(offerSummary)).toEqual({ @@ -36,6 +40,7 @@ describe('transformOfferSummary', () => { redeemedFunds: 100, remainingFunds: null, percentUtilized: null, + offerType: 'Site', }); }); }); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 22cff6cd3a..30b0efba01 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -37,6 +37,7 @@ export const transformOfferSummary = (offerSummary) => { if (percentUtilized) { percentUtilized = Math.min(percentUtilized, 1.0); } + const { offerType } = offerSummary; return { totalFunds, @@ -45,6 +46,7 @@ export const transformOfferSummary = (offerSummary) => { redeemedFundsExecEd, remainingFunds, percentUtilized, + offerType, }; }; diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index afa6c60620..49350821d1 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -15,6 +15,7 @@ import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import BudgetCard from '../BudgetCard'; import { useOfferSummary, useOfferRedemptions } from '../data/hooks'; +import { EXEC_ED_OFFER_TYPE } from '../data/constants'; jest.mock('../data/hooks'); useOfferSummary.mockReturnValue({ @@ -51,6 +52,7 @@ const mockOfferSummary = { redeemedFunds: 200, remainingFunds: 4800, percentUtilized: 0.04, + offerType: EXEC_ED_OFFER_TYPE, }; const BudgetCardWrapper = ({ ...rest }) => ( @@ -67,7 +69,7 @@ describe('', () => { jest.clearAllMocks(); }); - it('displays correctly', () => { + it('displays correctly for all offers', () => { const mockOffer = { id: mockEnterpriseOfferId, name: mockOfferDisplayName, @@ -105,6 +107,50 @@ describe('', () => { expect(firstElementWithTestId).toHaveTextContent(formattedString); }); + it('displays correctly for Offer type Site', () => { + const mockOffer = { + id: mockEnterpriseOfferId, + name: mockOfferDisplayName, + start: '2022-01-01', + end: '2023-01-01', + }; + const mockOfferRedemption = { + created: '2022-02-01', + enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, + }; + useOfferSummary.mockReturnValue({ + isLoading: false, + offerSummary: { + totalFunds: 5000, + redeemedFunds: 200, + remainingFunds: 4800, + percentUtilized: 0.04, + offerType: 'Site', + }, + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: { + results: [mockOfferRedemption], + itemCount: 1, + pageCount: 1, + }, + fetchOfferRedemptions: jest.fn(), + }); + render(); + expect(screen.getByText('Open Courses Marketplace')); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + expect(screen.getByText(`$${mockOfferSummary.redeemedFunds.toLocaleString()}`)); + const formattedString = `${dayjs(mockOffer.start).format('MMMM D, YYYY')} - ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('offer-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + }); + it('displays table on clicking view budget', async () => { const mockOffer = { id: mockEnterpriseOfferId, From 9af4648d73782d7e1eef0f674bce2b1ce7aaa266 Mon Sep 17 00:00:00 2001 From: Zaman Afzal Date: Thu, 24 Aug 2023 17:29:56 +0500 Subject: [PATCH 017/124] feat: Show one card on learner credit screen (#1022) --- .../BudgetCard-V2.jsx | 163 ++++++++++++++++++ .../LearnerCreditAllocationTable.jsx | 7 +- .../MultipleBudgetsPicker.jsx | 2 +- .../tests/BudgetCard.test.jsx | 44 +---- 4 files changed, 168 insertions(+), 48 deletions(-) create mode 100644 src/components/learner-credit-management/BudgetCard-V2.jsx diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx new file mode 100644 index 0000000000..16132bf012 --- /dev/null +++ b/src/components/learner-credit-management/BudgetCard-V2.jsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; +import { + Card, + Button, + Stack, + Row, + Col, + Breadcrumb, +} from '@edx/paragon'; + +import { useOfferRedemptions, useOfferSummary } from './data/hooks'; +import LearnerCreditAggregateCards from './LearnerCreditAggregateCards'; +import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; +import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; + +const BudgetCard = ({ + offer, + enterpriseUUID, + enterpriseSlug, +}) => { + const { + start, + end, + } = offer; + + const { + isLoading: isLoadingOfferSummary, + offerSummary, + } = useOfferSummary(enterpriseUUID, offer); + + const { + isLoading: isLoadingOfferRedemptions, + offerRedemptions, + fetchOfferRedemptions, + } = useOfferRedemptions(enterpriseUUID, offer?.id); + const [detailPage, setDetailPage] = useState(false); + const [activeLabel, setActiveLabel] = useState(''); + const links = [ + { label: 'Budgets', url: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}` }, + ]; + const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); + const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); + const navigateToBudgetRedemptions = (budgetType) => { + setDetailPage(true); + links.push({ label: budgetType, url: `/${enterpriseSlug}/admin/learner-credit` }); + setActiveLabel(budgetType); + }; + + const renderActions = (budgetType) => ( + + ); + + const renderCardHeader = (budgetType) => { + const subtitle = ( +
+ + {formattedStartDate} - {formattedExpirationDate} + +
+ ); + + return ( + + {renderActions(budgetType)} +
+ )} + /> + ); + }; + + const renderCardSection = (available, spent) => ( + + + + Available + {available} + + + Spent + {spent} + + + + ); + + const renderCardAggregate = () => ( +
+ +
+ ); + + return ( + + + + + + + {!detailPage + ? ( + <> + {renderCardAggregate()} +

Budgets

+ + + + {renderCardHeader('Overview')} + {renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFunds)} + + + + + ) + : ( + + )} +
+ ); +}; + +BudgetCard.propTypes = { + offer: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + start: PropTypes.string.isRequired, + end: PropTypes.string.isRequired, + }).isRequired, + enterpriseUUID: PropTypes.string.isRequired, + enterpriseSlug: PropTypes.string.isRequired, +}; + +export default BudgetCard; diff --git a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx index e463487937..e5eceb7766 100644 --- a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx +++ b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx @@ -17,10 +17,9 @@ const LearnerCreditAllocationTable = ({ tableData, fetchTableData, enterpriseUUID, - budgetType, }) => { const isDesktopTable = useMediaQuery({ minWidth: breakpoints.extraLarge.minWidth }); - const defaultFilter = budgetType ? [{ id: 'courseProductLine', value: budgetType }] : []; + const defaultFilter = []; return ( ); }; -LearnerCreditAllocationTable.defaultProps = { - budgetType: null, -}; LearnerCreditAllocationTable.propTypes = { enterpriseUUID: PropTypes.string.isRequired, @@ -109,7 +105,6 @@ LearnerCreditAllocationTable.propTypes = { pageCount: PropTypes.number.isRequired, }).isRequired, fetchTableData: PropTypes.func.isRequired, - budgetType: PropTypes.string, }; export default LearnerCreditAllocationTable; diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index a7b4bc814e..8535bd1d21 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -6,7 +6,7 @@ import { Col, } from '@edx/paragon'; -import BudgetCard from './BudgetCard'; +import BudgetCard from './BudgetCard-V2'; const MultipleBudgetsPicker = ({ offers, diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 49350821d1..cfc91ded7c 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -13,7 +13,7 @@ import { import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import BudgetCard from '../BudgetCard'; +import BudgetCard from '../BudgetCard-V2'; import { useOfferSummary, useOfferRedemptions } from '../data/hooks'; import { EXEC_ED_OFFER_TYPE } from '../data/constants'; @@ -69,45 +69,7 @@ describe('', () => { jest.clearAllMocks(); }); - it('displays correctly for all offers', () => { - const mockOffer = { - id: mockEnterpriseOfferId, - name: mockOfferDisplayName, - start: '2022-01-01', - end: '2023-01-01', - }; - const mockOfferRedemption = { - created: '2022-02-01', - enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, - }; - useOfferSummary.mockReturnValue({ - isLoading: false, - offerSummary: mockOfferSummary, - }); - useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: { - results: [mockOfferRedemption], - itemCount: 1, - pageCount: 1, - }, - fetchOfferRedemptions: jest.fn(), - }); - render(); - expect(screen.getByText('Open Courses Marketplace')); - expect(screen.getByText('Executive Education')); - expect(screen.getByText(`$${mockOfferSummary.redeemedFunds.toLocaleString()}`)); - const formattedString = `${dayjs(mockOffer.start).format('MMMM D, YYYY')} - ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; - const elementsWithTestId = screen.getAllByTestId('offer-date'); - const firstElementWithTestId = elementsWithTestId[0]; - expect(firstElementWithTestId).toHaveTextContent(formattedString); - }); - - it('displays correctly for Offer type Site', () => { + it('displays correctly for Offers', () => { const mockOffer = { id: mockEnterpriseOfferId, name: mockOfferDisplayName, @@ -142,7 +104,7 @@ describe('', () => { enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseId} />); - expect(screen.getByText('Open Courses Marketplace')); + expect(screen.getByText('Overview')); expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); expect(screen.getByText(`$${mockOfferSummary.redeemedFunds.toLocaleString()}`)); const formattedString = `${dayjs(mockOffer.start).format('MMMM D, YYYY')} - ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; From ef1692d0541cd160fbd700a9cde4f3cf7d493f6c Mon Sep 17 00:00:00 2001 From: Emily Rosario-Aquin <129111440+emrosarioa@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:43:12 -0500 Subject: [PATCH 018/124] Refactor LearnerCreditAllocationTable (#1019) * chore: refactor LCM data table --- .../BudgetCard-V2.jsx | 1 + .../LearnerCreditAllocationTable.jsx | 54 ++++++++++++------- .../learner-credit-management/data/hooks.js | 13 ++--- .../data/tests/hooks.test.js | 6 +-- .../learner-credit-management/data/utils.js | 1 + .../tests/BudgetCard.test.jsx | 1 - .../LearnerCreditAllocationTable.test.jsx | 38 ++++++++++++- 7 files changed, 79 insertions(+), 35 deletions(-) diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx index 16132bf012..d9a1db40c9 100644 --- a/src/components/learner-credit-management/BudgetCard-V2.jsx +++ b/src/components/learner-credit-management/BudgetCard-V2.jsx @@ -143,6 +143,7 @@ const BudgetCard = ({ tableData={offerRedemptions} fetchTableData={fetchOfferRedemptions} enterpriseUUID={enterpriseUUID} + enterpriseSlug={enterpriseSlug} /> )} diff --git a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx index e5eceb7766..7422b41594 100644 --- a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx +++ b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx @@ -1,10 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; -import { - DataTable, useMediaQuery, breakpoints, -} from '@edx/paragon'; - +import { DataTable, Hyperlink } from '@edx/paragon'; +import { getConfig } from '@edx/frontend-platform/config'; import TableTextFilter from './TableTextFilter'; import EmailAddressTableCell from './EmailAddressTableCell'; import { getCourseProductLineText } from '../../utils'; @@ -12,13 +10,19 @@ import { getCourseProductLineText } from '../../utils'; export const PAGE_SIZE = 20; export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array +const getEnrollmentDetailsAccessor = row => ({ + courseTitle: row.courseTitle, + userEmail: row.userEmail, + courseKey: row.courseKey, +}); + const LearnerCreditAllocationTable = ({ isLoading, tableData, fetchTableData, enterpriseUUID, + enterpriseSlug, }) => { - const isDesktopTable = useMediaQuery({ minWidth: breakpoints.extraLarge.minWidth }); const defaultFilter = []; return ( @@ -29,32 +33,41 @@ const LearnerCreditAllocationTable = ({ manualPagination isFilterable manualFilters - showFiltersInSidebar={isDesktopTable} isLoading={isLoading} defaultColumnValues={{ Filter: TableTextFilter }} + /* eslint-disable */ columns={[ { - Header: 'Email Address', - accessor: 'userEmail', - // eslint-disable-next-line react/prop-types, react/no-unstable-nested-components - Cell: ({ row }) => , + Header: 'Date', + accessor: 'enrollmentDate', + Cell: ({ row }) => dayjs(row.values.enrollmentDate).format('MMM D, YYYY'), + disableFilters: true, }, { - Header: 'Course Name', - accessor: 'courseTitle', + Header: 'Enrollment details', + accessor: getEnrollmentDetailsAccessor, + Cell: ({ row }) => ( + <> + +
+ + {row.original.courseTitle} + +
+ + ), + disableFilters: false, + disableSortBy: true, }, { - Header: 'Course Price', + Header: 'Amount', accessor: 'courseListPrice', Cell: ({ row }) => `$${row.values.courseListPrice}`, disableFilters: true, }, - { - Header: 'Date Spent', - accessor: 'enrollmentDate', - Cell: ({ row }) => dayjs(row.values.enrollmentDate).format('MMMM DD, YYYY'), - disableFilters: true, - }, { Header: 'Product', accessor: 'courseProductLine', @@ -78,7 +91,6 @@ const LearnerCreditAllocationTable = ({ itemCount={tableData.itemCount} pageCount={tableData.pageCount} EmptyTableComponent={ - // eslint-disable-next-line react/no-unstable-nested-components () => { if (isLoading) { return null; @@ -89,9 +101,11 @@ const LearnerCreditAllocationTable = ({ /> ); }; +/* eslint-enable */ LearnerCreditAllocationTable.propTypes = { enterpriseUUID: PropTypes.string.isRequired, + enterpriseSlug: PropTypes.string.isRequired, isLoading: PropTypes.bool.isRequired, tableData: PropTypes.shape({ results: PropTypes.arrayOf(PropTypes.shape({ diff --git a/src/components/learner-credit-management/data/hooks.js b/src/components/learner-credit-management/data/hooks.js index 7ff74aaa69..585970c35e 100644 --- a/src/components/learner-credit-management/data/hooks.js +++ b/src/components/learner-credit-management/data/hooks.js @@ -63,18 +63,15 @@ const applySortByToOptions = (sortBy, options) => { }; const applyFiltersToOptions = (filters, options) => { - const userSearchQuery = filters?.find(filter => filter.id === 'userEmail')?.value; - const courseTitleSearchQuery = filters?.find(filter => filter.id === 'courseTitle')?.value; const courseProductLineSearchQuery = filters?.find(filter => filter.id === 'courseProductLine')?.value; - if (userSearchQuery) { - Object.assign(options, { search: userSearchQuery }); - } - if (courseTitleSearchQuery) { - Object.assign(options, { searchCourse: courseTitleSearchQuery }); - } + const searchQuery = filters?.find(filter => filter.id.toLowerCase() === 'enrollment details')?.value; + if (courseProductLineSearchQuery) { Object.assign(options, { courseProductLine: courseProductLineSearchQuery }); } + if (searchQuery) { + Object.assign(options, { searchAll: searchQuery }); + } }; export const useOfferRedemptions = (enterpriseUUID, offerId) => { diff --git a/src/components/learner-credit-management/data/tests/hooks.test.js b/src/components/learner-credit-management/data/tests/hooks.test.js index fa03c60f9e..8ab61bce2f 100644 --- a/src/components/learner-credit-management/data/tests/hooks.test.js +++ b/src/components/learner-credit-management/data/tests/hooks.test.js @@ -105,8 +105,7 @@ describe('useOfferRedemptions', () => { { id: 'enrollmentDate', desc: true }, ], filters: [ - { id: 'userEmail', value: mockOfferEnrollments[0].user_email }, - { id: 'courseTitle', value: mockOfferEnrollments[0].course_title }, + { id: 'Enrollment Details', value: mockOfferEnrollments[0].user_email }, ], }); }); @@ -118,8 +117,7 @@ describe('useOfferRedemptions', () => { pageSize: 20, offerId: mockEnterpriseOffer.id, ordering: '-enrollment_date', // default sort order - search: mockOfferEnrollments[0].user_email, - searchCourse: mockOfferEnrollments[0].course_title, + searchAll: mockOfferEnrollments[0].user_email, ignoreNullCourseListPrice: true, }; expect(EnterpriseDataApiService.fetchCourseEnrollments).toHaveBeenCalledWith( diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 30b0efba01..65524c1346 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -68,6 +68,7 @@ export const transformUtilizationTableResults = results => results.map(result => enrollmentDate: result.enrollmentDate, courseProductLine: result.courseProductLine, uuid: uuidv4(), + courseKey: result.courseKey, })); /** diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index cfc91ded7c..7d8f349bda 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -141,7 +141,6 @@ describe('', () => { const elementsWithTestId = screen.getAllByTestId('view-budget'); const firstElementWithTestId = elementsWithTestId[0]; await waitFor(() => userEvent.click(firstElementWithTestId)); - expect(screen.getByText('Filters')); expect(screen.getByText('No results found')); }); }); diff --git a/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx index fb79748c70..f67aa0e8bb 100644 --- a/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx +++ b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx @@ -7,6 +7,10 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import LearnerCreditAllocationTable from '../LearnerCreditAllocationTable'; +jest.mock('@edx/frontend-platform/config', () => ({ + getConfig: () => ({ ENTERPRISE_LEARNER_PORTAL_URL: 'https://enterprise.edx.org' }), +})); + const LearnerCreditAllocationTableWrapper = (props) => ( @@ -19,6 +23,7 @@ describe('', () => { enterpriseUUID: 'test-enterprise-id', isLoading: false, budgetType: 'OCM', + enterpriseSlug: 'test-enterprise-slug', tableData: { results: [{ userEmail: 'test@example.com', @@ -36,7 +41,6 @@ describe('', () => { render(); - expect(screen.getByText('Open', { exact: false })); expect(screen.getByText(props.tableData.results[0].userEmail.toString(), { exact: false, })); @@ -46,7 +50,7 @@ describe('', () => { expect(screen.getByText(props.tableData.results[0].courseListPrice.toString(), { exact: false, })); - expect(screen.getByText('February', { exact: false })); + expect(screen.getByText('Feb', { exact: false })); }); it('renders with empty table data', () => { const props = { @@ -66,4 +70,34 @@ describe('', () => { expect(screen.getByText('No results found', { exact: false })); }); + + it('constructs the correct URL for the course', () => { + const props = { + enterpriseUUID: 'test-enterprise-id', + isLoading: false, + budgetType: 'OCM', + enterpriseSlug: 'test-enterprise-slug', + tableData: { + results: [{ + userEmail: 'test@example.com', + courseTitle: 'course-title', + courseKey: 'course-v1:edX=CTL.SC101x.3T2019', + courseListPrice: 100, + enrollmentDate: '2-2-23', + courseProductLine: 'OCM', + }], + itemCount: 1, + pageCount: 1, + }, + fetchTableData: jest.fn(), + }; + props.fetchTableData.mockReturnValue(props.tableData); + + render(); + + const expectedLink = 'https://enterprise.edx.org/test-enterprise-slug/course/course-v1:edX=CTL.SC101x.3T2019'; + const courseLinkElement = screen.getByText('course-title'); + + expect(courseLinkElement.getAttribute('href')).toBe(expectedLink); + }); }); From 0979d48ff37cfedc48c560c913b47e3911272b55 Mon Sep 17 00:00:00 2001 From: Marlon Keating Date: Wed, 30 Aug 2023 19:59:50 +0000 Subject: [PATCH 019/124] feat: Add feature flag AUTH0_SELF_SERVICE_INTEGRATION chore: clean up comments --- .../SettingsSSOTab/NewSSOConfigForm.jsx | 5 ++++- .../settings/SettingsSSOTab/NewSSOStepper.jsx | 3 +++ .../tests/NewSSOConfigForm.test.jsx | 21 +++++++++++++++++++ src/config/index.js | 1 + 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/components/settings/SettingsSSOTab/NewSSOStepper.jsx diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigForm.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigForm.jsx index cac4e0ee98..6cbcab4c14 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigForm.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigForm.jsx @@ -4,16 +4,19 @@ import { WarningFilled } from '@edx/paragon/icons'; import { SSOConfigContext } from './SSOConfigContext'; import SSOStepper from './SSOStepper'; import { HELP_CENTER_SAML_LINK } from '../data/constants'; +import { features } from '../../../config'; +import NewSSOStepper from './NewSSOStepper'; const NewSSOConfigForm = () => { const { ssoState: { currentError } } = useContext(SSOConfigContext); + const { AUTH0_SELF_SERVICE_INTEGRATION } = features; return (
Connect to a SAML identity provider for single sign-on to allow quick access to your organization's learning catalog. - + {AUTH0_SELF_SERVICE_INTEGRATION ? : } {currentError && ( null; + +export default NewSSOStepper; diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx index 891c102f0d..03372c4fd5 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx @@ -12,6 +12,7 @@ import handleErrors from '../../utils'; import { INVALID_ODATA_API_TIMEOUT_INTERVAL, INVALID_SAPSF_OAUTH_ROOT_URL, INVALID_API_ROOT_URL, } from '../../data/constants'; +import { features } from '../../../../config'; jest.mock('../data/actions'); jest.mock('../../utils'); @@ -70,6 +71,7 @@ const contextValue = { describe('SAML Config Tab', () => { afterEach(() => { + features.AUTH0_SELF_SERVICE_INTEGRATION = false; jest.clearAllMocks(); }); test('canceling connect step', async () => { @@ -307,6 +309,25 @@ describe('SAML Config Tab', () => { expect(screen.getByText('Next')).not.toBeDisabled(); }, []); }); + test('show new SSO stepper placeholder when feature flag enabled', async () => { + // Setup + features.AUTH0_SELF_SERVICE_INTEGRATION = true; + contextValue.ssoState.currentStep = 'idp'; + render( + + + , + ); + await waitFor(() => { + expect( + screen.queryByText( + 'Connect to a SAML identity provider for single sign-on' + + ' to allow quick access to your organization\'s learning catalog.', + ), + ).toBeInTheDocument(); + expect(screen.queryByText('Next')).not.toBeInTheDocument(); + }, []); + }); test('idp step fetches and displays existing idp data fields', async () => { // Setup const mockGetProviderData = jest.spyOn(LmsApiService, 'getProviderData'); diff --git a/src/config/index.js b/src/config/index.js index 4fe281804e..663f190723 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -53,6 +53,7 @@ const features = { FEATURE_SSO_SETTINGS_TAB: process.env.FEATURE_SSO_SETTINGS_TAB || hasFeatureFlagEnabled('SSO_SETTINGS_TAB'), FEATURE_INTEGRATION_REPORTING: process.env.FEATURE_INTEGRATION_REPORTING || hasFeatureFlagEnabled('FEATURE_INTEGRATION_REPORTING'), SUBSCRIPTION_LPR: process.env.SUBSCRIPTION_LPR || hasFeatureFlagEnabled('SUBSCRIPTION_LPR'), + AUTH0_SELF_SERVICE_INTEGRATION: process.env.AUTH0_SELF_SERVICE_INTEGRATION || hasFeatureFlagEnabled('AUTH0_SELF_SERVICE_INTEGRATION'), }; export { configuration, features }; From 5a629a57896cc96f1118f4b960096febf0f49a90 Mon Sep 17 00:00:00 2001 From: Emily Rosario-Aquin <129111440+emrosarioa@users.noreply.github.com> Date: Fri, 1 Sep 2023 08:44:32 -0500 Subject: [PATCH 020/124] ENT-7512: LearnerCreditAllocationTable course title (#1025) * chore: courseTitle link in LCM only if learner portal enabled --- .../BudgetCard-V2.jsx | 3 ++ .../LearnerCreditAllocationTable.jsx | 9 ++++++ .../MultipleBudgetsPage.jsx | 4 +++ .../MultipleBudgetsPicker.jsx | 3 ++ .../LearnerCreditAllocationTable.test.jsx | 30 +++++++++++++++++++ 5 files changed, 49 insertions(+) diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx index d9a1db40c9..b39b9297d9 100644 --- a/src/components/learner-credit-management/BudgetCard-V2.jsx +++ b/src/components/learner-credit-management/BudgetCard-V2.jsx @@ -19,6 +19,7 @@ const BudgetCard = ({ offer, enterpriseUUID, enterpriseSlug, + enableLearnerPortal, }) => { const { start, @@ -144,6 +145,7 @@ const BudgetCard = ({ fetchTableData={fetchOfferRedemptions} enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseSlug} + enableLearnerPortal={enableLearnerPortal} /> )} @@ -159,6 +161,7 @@ BudgetCard.propTypes = { }).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, + enableLearnerPortal: PropTypes.bool.isRequired, }; export default BudgetCard; diff --git a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx index 7422b41594..555df1ca95 100644 --- a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx +++ b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx @@ -16,12 +16,15 @@ const getEnrollmentDetailsAccessor = row => ({ courseKey: row.courseKey, }); +const FilterStatus = (rest) => ; + const LearnerCreditAllocationTable = ({ isLoading, tableData, fetchTableData, enterpriseUUID, enterpriseSlug, + enableLearnerPortal, }) => { const defaultFilter = []; @@ -35,6 +38,7 @@ const LearnerCreditAllocationTable = ({ manualFilters isLoading={isLoading} defaultColumnValues={{ Filter: TableTextFilter }} + FilterStatusComponent={FilterStatus} /* eslint-disable */ columns={[ { @@ -50,12 +54,16 @@ const LearnerCreditAllocationTable = ({ <>
+ {enableLearnerPortal ? ( {row.original.courseTitle} + ) : ( + row.original.courseTitle + )}
), @@ -106,6 +114,7 @@ const LearnerCreditAllocationTable = ({ LearnerCreditAllocationTable.propTypes = { enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, + enableLearnerPortal: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired, tableData: PropTypes.shape({ results: PropTypes.arrayOf(PropTypes.shape({ diff --git a/src/components/learner-credit-management/MultipleBudgetsPage.jsx b/src/components/learner-credit-management/MultipleBudgetsPage.jsx index cec507a231..3df18c465a 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPage.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPage.jsx @@ -22,6 +22,7 @@ const PAGE_TITLE = 'Learner Credit'; const MultipleBudgetsPage = ({ enterpriseUUID, enterpriseSlug, + enableLearnerPortal, }) => { const { offers, isLoading } = useContext(EnterpriseSubsidiesContext); @@ -66,6 +67,7 @@ const MultipleBudgetsPage = ({ offers={offers} enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseSlug} + enableLearnerPortal={enableLearnerPortal} /> ); @@ -74,11 +76,13 @@ const MultipleBudgetsPage = ({ const mapStateToProps = state => ({ enterpriseUUID: state.portalConfiguration.enterpriseId, enterpriseSlug: state.portalConfiguration.enterpriseSlug, + enableLearnerPortal: state.portalConfiguration.enableLearnerPortal, }); MultipleBudgetsPage.propTypes = { enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, + enableLearnerPortal: PropTypes.bool.isRequired, }; export default connect(mapStateToProps)(MultipleBudgetsPage); diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index 8535bd1d21..4c3da2d0ce 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -12,6 +12,7 @@ const MultipleBudgetsPicker = ({ offers, enterpriseUUID, enterpriseSlug, + enableLearnerPortal, }) => ( @@ -22,6 +23,7 @@ const MultipleBudgetsPicker = ({ offer={offer} enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseSlug} + enableLearnerPortal={enableLearnerPortal} /> ))} @@ -33,6 +35,7 @@ MultipleBudgetsPicker.propTypes = { offers: PropTypes.arrayOf(PropTypes.shape()).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, + enableLearnerPortal: PropTypes.bool.isRequired, }; export default MultipleBudgetsPicker; diff --git a/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx index f67aa0e8bb..9099404e4f 100644 --- a/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx +++ b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx @@ -24,6 +24,7 @@ describe('', () => { isLoading: false, budgetType: 'OCM', enterpriseSlug: 'test-enterprise-slug', + enableLearnerPortal: true, tableData: { results: [{ userEmail: 'test@example.com', @@ -77,6 +78,7 @@ describe('', () => { isLoading: false, budgetType: 'OCM', enterpriseSlug: 'test-enterprise-slug', + enableLearnerPortal: true, tableData: { results: [{ userEmail: 'test@example.com', @@ -100,4 +102,32 @@ describe('', () => { expect(courseLinkElement.getAttribute('href')).toBe(expectedLink); }); + + it('does not render the course link if the learner portal is disabled', () => { + const props = { + enterpriseUUID: 'test-enterprise-id', + isLoading: false, + budgetType: 'OCM', + enterpriseSlug: 'test-enterprise-slug', + enableLearnerPortal: false, + tableData: { + results: [{ + userEmail: 'test@example.com', + courseTitle: 'course-title', + courseKey: 'course-v1:edX=CTL.SC101x.3T2019', + courseListPrice: 100, + enrollmentDate: '2-2-23', + courseProductLine: 'OCM', + }], + itemCount: 1, + pageCount: 1, + }, + fetchTableData: jest.fn(), + }; + props.fetchTableData.mockReturnValue(props.tableData); + + render(); + const courseTitleElement = screen.queryByText('course-title'); + expect(courseTitleElement.closest('a')).toBeNull(); + }); }); From 4da552cbdfcc86d3e48b5342ff8714bf504e77c2 Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Fri, 8 Sep 2023 09:53:15 -0600 Subject: [PATCH 021/124] feat: generate API Credentials in Admin Portal (#1027) * feat: adding api credential tab * feat: add zero state card under api credentails tab * feat: add a new tab * fix: make lms-service run as expected * fix: fix coupon.test.jsx lint error * feat: generate API Credentials Tab in Admin Portal * fix: modify modal * fix: modify lmsservice url * fix: remove dependency in useffect * fix: add api-document url * fix: lots of little fixes * fix: test fixes * fix: more changes * fix: more fixes * fix: PR review requests --- .env.development | 1 + .../ContactCustomerSupportButton/index.jsx | 2 +- src/components/forms/FormWorkflow.tsx | 11 +- src/components/settings/HelpCenterButton.jsx | 33 ++ .../APICredentialsPage.jsx | 98 ++++++ .../SettingsApiCredentialsTab/Context.jsx | 5 + .../SettingsApiCredentialsTab/CopiedToast.jsx | 13 + .../SettingsApiCredentialsTab/CopyButton.jsx | 48 +++ .../SettingsApiCredentialsTab/FailedAlert.jsx | 16 + .../RegenerateCredentialWarningModal.jsx | 98 ++++++ .../ZeroStateCard.jsx | 91 ++++++ .../SettingsApiCredentialsTab/constants.jsx | 14 + .../SettingsApiCredentialsTab/index.jsx | 70 +++++ .../tests/SettingsAPICredentialsPage.test.jsx | 281 ++++++++++++++++++ .../settings/SettingsLMSTab/index.jsx | 11 +- src/components/settings/SettingsTabs.jsx | 57 +++- src/components/settings/data/constants.js | 9 + src/components/settings/settings.scss | 15 +- .../settings/tests/SettingsTabs.test.jsx | 14 + src/config/index.js | 1 + src/data/images/ZeroState.svg | 45 +++ src/data/reducers/portalConfiguration.js | 4 + src/data/reducers/portalConfiguration.test.js | 3 + src/data/services/LmsApiService.js | 17 ++ 24 files changed, 924 insertions(+), 33 deletions(-) create mode 100644 src/components/settings/HelpCenterButton.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/Context.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/constants.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/index.jsx create mode 100644 src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx create mode 100644 src/data/images/ZeroState.svg diff --git a/.env.development b/.env.development index d74a56083c..03858d1ff4 100644 --- a/.env.development +++ b/.env.development @@ -40,6 +40,7 @@ FEATURE_SETTINGS_PAGE_LMS_TAB='true' FEATURE_SETTINGS_PAGE_APPEARANCE_TAB='true' FEATURE_LEARNER_CREDIT_MANAGEMENT='true' FEATURE_CONTENT_HIGHLIGHTS='true' +FEATURE_API_CREDENTIALS_TAB='true' HOTJAR_APP_ID='' HOTJAR_VERSION='6' HOTJAR_DEBUG='' diff --git a/src/components/ContactCustomerSupportButton/index.jsx b/src/components/ContactCustomerSupportButton/index.jsx index 8c2e1408ca..b0bdee42cf 100644 --- a/src/components/ContactCustomerSupportButton/index.jsx +++ b/src/components/ContactCustomerSupportButton/index.jsx @@ -31,7 +31,7 @@ ContactCustomerSupportButton.propTypes = { ContactCustomerSupportButton.defaultProps = { children: 'Contact support', - variant: 'btn-outline-primary', + variant: 'outline-primary', }; export default ContactCustomerSupportButton; diff --git a/src/components/forms/FormWorkflow.tsx b/src/components/forms/FormWorkflow.tsx index e87c12a931..f6c2a9d6da 100644 --- a/src/components/forms/FormWorkflow.tsx +++ b/src/components/forms/FormWorkflow.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import type { Dispatch } from 'react'; import { - ActionRow, Button, FullscreenModal, Hyperlink, Stepper, useToggle, + ActionRow, Button, FullscreenModal, Stepper, useToggle, } from '@edx/paragon'; import { Launch } from '@edx/paragon/icons'; @@ -14,6 +14,7 @@ import { HELP_CENTER_LINK, SUBMIT_TOAST_MESSAGE } from '../settings/data/constan import UnsavedChangesModal from '../settings/SettingsLMSTab/UnsavedChangesModal'; import ConfigErrorModal from '../settings/ConfigErrorModal'; import { channelMapping, pollAsync } from '../../utils'; +import HelpCenterButton from '../settings/HelpCenterButton'; export const WAITING_FOR_ASYNC_OPERATION = 'WAITING FOR ASYNC OPERATION'; @@ -201,13 +202,9 @@ const FormWorkflow = ({ className="stepper-modal" footerNode={( - + Help Center: Integrations - + {nextButtonConfig && ( diff --git a/src/components/settings/HelpCenterButton.jsx b/src/components/settings/HelpCenterButton.jsx new file mode 100644 index 0000000000..50b9749abb --- /dev/null +++ b/src/components/settings/HelpCenterButton.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Hyperlink } from '@edx/paragon'; +import PropTypes from 'prop-types'; + +const HelpCenterButton = ({ + url, + children, + ...rest +}) => { + const destinationUrl = url; + + return ( + + {children} + + ); +}; + +HelpCenterButton.defaultProps = { + children: 'Help Center', +}; + +HelpCenterButton.propTypes = { + children: PropTypes.node, + url: PropTypes.string, +}; + +export default HelpCenterButton; diff --git a/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx b/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx new file mode 100644 index 0000000000..f86dc4aaaf --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx @@ -0,0 +1,98 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import { Form, Hyperlink } from '@edx/paragon'; +import { dataPropType } from './constants'; +import RegenerateCredentialWarningModal from './RegenerateCredentialWarningModal'; +import CopyButton from './CopyButton'; +import { API_CLIENT_DOCUMENTATION, HELP_CENTER_LINK } from '../data/constants'; + +const APICredentialsPage = ({ data, setData }) => { + const [formValue, setFormValue] = useState(''); + const handleFormChange = (e) => { + setFormValue(e.target.value); + }; + return ( +
+
+

Your API credentials

+

+ Copy and paste the following credential information and send it to your API developer(s). +

+
+
+

+ Application name: + {data?.name} +

+

+ Allowed URIs: + {data?.redirect_uris} +

+

+ API client ID: + {data?.client_id} +

+

+ API client secret: + {data?.client_secret} +

+

API client documentation: + {API_CLIENT_DOCUMENTATION} +

+

+ Last generated on: + {data?.updated} +

+
+ +
+
+
+

Redirect URIs (optional)

+

+ If you need additional redirect URIs, add them below and regenerate your API credentials. + You will need to communicate the new credentials to your API developers. +

+ +

+ Allowed URIs list, space separated +

+ +
+
+

Questions or modifications?

+

+ To troubleshoot your API credentialing, or to request additional API endpoints to your + credentials,  + + contact Enterprise Customer Support. + +

+
+
+ ); +}; + +APICredentialsPage.defaultProps = { + data: null, +}; + +APICredentialsPage.propTypes = { + data: PropTypes.shape(dataPropType), + setData: PropTypes.func.isRequired, +}; + +export default APICredentialsPage; diff --git a/src/components/settings/SettingsApiCredentialsTab/Context.jsx b/src/components/settings/SettingsApiCredentialsTab/Context.jsx new file mode 100644 index 0000000000..4682f99ef7 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/Context.jsx @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +export const ErrorContext = createContext(null); +export const ShowSuccessToast = createContext(null); +export const EnterpriseId = createContext(null); diff --git a/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx b/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx new file mode 100644 index 0000000000..5506b7e14f --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Toast } from '@edx/paragon'; + +const CopiedToast = ({ content, ...rest }) => ( + + {content} + +); +CopiedToast.propTypes = { + content: PropTypes.string.isRequired, +}; +export default CopiedToast; diff --git a/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx b/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx new file mode 100644 index 0000000000..eb3477e047 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import { Button } from '@edx/paragon'; +import { ContentCopy } from '@edx/paragon/icons'; +import CopiedToast from './CopiedToast'; +import { dataPropType } from './constants'; + +const CopyButton = ({ data }) => { + const [isCopyLinkToastOpen, setIsCopyLinkToastOpen] = useState(false); + const [copiedError, setCopiedError] = useState(false); + + const handleCopyLink = async () => { + try { + const jsonString = JSON.stringify(data); + await navigator.clipboard.writeText(jsonString); + } catch (error) { + setCopiedError(true); + } finally { + setIsCopyLinkToastOpen(true); + } + }; + const handleCloseLinkCopyToast = () => { + setIsCopyLinkToastOpen(false); + }; + return ( + <> + + + + ); +}; + +CopyButton.propTypes = { + data: PropTypes.shape(dataPropType), +}; + +export default CopyButton; diff --git a/src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx b/src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx new file mode 100644 index 0000000000..f37c285dc1 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx @@ -0,0 +1,16 @@ +import { Alert } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; +import { credentialErrorMessage } from './constants'; + +const FailedAlert = () => ( + + + Credential generation failed + +

+ {credentialErrorMessage} +

+
+); + +export default FailedAlert; diff --git a/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx new file mode 100644 index 0000000000..dd829012d5 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/RegenerateCredentialWarningModal.jsx @@ -0,0 +1,98 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { + ActionRow, Button, Icon, ModalDialog, useToggle, +} from '@edx/paragon'; +import { Warning } from '@edx/paragon/icons'; + +import { + ErrorContext, + ShowSuccessToast, EnterpriseId, +} from './Context'; +import LmsApiService from '../../../data/services/LmsApiService'; +import { dataPropType } from './constants'; + +const RegenerateCredentialWarningModal = ({ + redirectURIs, + data, + setData, +}) => { + const [isOn, setOn, setOff] = useToggle(false); + const [, setHasError] = useContext(ErrorContext); + const [, setShowSuccessToast] = useContext(ShowSuccessToast); + const enterpriseId = useContext(EnterpriseId); + const handleOnClickRegeneration = async () => { + try { + const response = await LmsApiService.regenerateAPICredentials(redirectURIs, enterpriseId); + const newURIs = response.data.redirect_uris; + setShowSuccessToast(true); + const updatedData = data; + updatedData.redirect_uris = newURIs; + setData(updatedData); + } catch (error) { + setHasError(true); + } finally { + setOff(true); + } + }; + + return ( + <> + + + + +
+ + Regenerate API credentials? +
+
+
+ +

+ Any system, job, or script using the previous credentials will no + longer be able to authenticate with the edX API. +

+

+ If you do regenerate, you will need to send the new credentials to your developers. +

+
+ + + + Cancel + + + + +
+ + ); +}; + +RegenerateCredentialWarningModal.propTypes = { + redirectURIs: PropTypes.string.isRequired, + data: PropTypes.shape(dataPropType), + setData: PropTypes.func.isRequired, +}; + +export default RegenerateCredentialWarningModal; diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx new file mode 100644 index 0000000000..a9e37a0b8f --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -0,0 +1,91 @@ +import React, { useState, useContext } from 'react'; +import PropTypes from 'prop-types'; + +import { + Button, Card, Hyperlink, Icon, Spinner, +} from '@edx/paragon'; +import { Add, Error } from '@edx/paragon/icons'; + +import { credentialErrorMessage } from './constants'; +import cardImage from '../../../data/images/ZeroState.svg'; +import { EnterpriseId } from './Context'; +import LmsApiService from '../../../data/services/LmsApiService'; +import { + API_CLIENT_DOCUMENTATION, API_TERMS_OF_SERVICE, HELP_CENTER_LINK, +} from '../data/constants'; + +const ZeroStateCard = ({ setShowToast, setData }) => { + const [isLoading, setIsLoading] = useState(false); + const [displayFailureAlert, setFailureAlert] = useState(false); + const enterpriseId = useContext(EnterpriseId); + const handleClick = async () => { + setIsLoading(true); + try { + const response = await LmsApiService.createNewAPICredentials(enterpriseId); + const data = { ...response.data, api_client_documentation: API_CLIENT_DOCUMENTATION }; + setData(data); + setShowToast(true); + } catch (err) { + setFailureAlert(true); + } + }; + + return ( + + + +

You don't have API credentials yet.

+ { !displayFailureAlert && ( +

+ This page allows you to generate API credentials to send to  + your developers so they can work on integration projects. + If you believe you are seeing this page in error,  + + contact Enterprise Customer Support. + +

+ )} +

+ edX for Business API credentials credentials will provide access  + to the following edX API endpoints: reporting dashboard, dashboard, and catalog administration. +

+

+ By clicking the button below, you and your organization accept the {'\n'} + edX API terms of service. +

+
+ + { displayFailureAlert && ( +

+ + {credentialErrorMessage} +

+ )} + +
+
+ ); +}; + +ZeroStateCard.propTypes = { + setShowToast: PropTypes.func.isRequired, + setData: PropTypes.func.isRequired, +}; + +export default ZeroStateCard; diff --git a/src/components/settings/SettingsApiCredentialsTab/constants.jsx b/src/components/settings/SettingsApiCredentialsTab/constants.jsx new file mode 100644 index 0000000000..2132f801df --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/constants.jsx @@ -0,0 +1,14 @@ +import PropTypes from 'prop-types'; + +export const dataPropType = PropTypes.shape({ + name: PropTypes.string, + redirect_uris: PropTypes.string, + client_id: PropTypes.string, + client_secret: PropTypes.string, + api_client_documentation: PropTypes.string, + updated: PropTypes.bool, +}); + +export const credentialErrorMessage = 'Something went wrong while ' ++ 'generating your credentials. Please try again. ' ++ 'If the issue continues, contact Enterprise Customer Support.'; diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx new file mode 100644 index 0000000000..2cdef4d762 --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -0,0 +1,70 @@ +/* eslint-disable react/jsx-no-constructed-context-values */ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { logError } from '@edx/frontend-platform/logging'; +import { ActionRow, Toast } from '@edx/paragon'; +import ZeroStateCard from './ZeroStateCard'; +import APICredentialsPage from './APICredentialsPage'; +import FailedAlert from './FailedAlert'; +import { HELP_CENTER_API_GUIDE } from '../data/constants'; +import HelpCenterButton from '../HelpCenterButton'; +import { + EnterpriseId, ErrorContext, ShowSuccessToast, +} from './Context'; +import LmsApiService from '../../../data/services/LmsApiService'; + +const SettingsApiCredentialsTab = ({ + enterpriseId, +}) => { + const [data, setData] = useState(); + const [hasRegenerationError, setHasRegenerationError] = useState(false); + const [showToast, setShowToast] = useState(false); + + useEffect(() => { + const fetchExistingAPICredentials = async () => { + try { + const response = await LmsApiService.fetchAPICredentials(enterpriseId); + setData(response.data); + } catch (error) { + logError(error); + } + }; + fetchExistingAPICredentials(); + }, [enterpriseId]); + + return ( + + + + { hasRegenerationError && } + +

API credentials

+ + + Help Center: EdX Enterprise API Guide + +
+
+ {!data ? ( + + ) : ()} +
+
+ { showToast && ( + setShowToast(false)} + show={showToast} + > + API credentials successfully generated + + )} + + + + ); +}; +SettingsApiCredentialsTab.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; +export default SettingsApiCredentialsTab; diff --git a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx new file mode 100644 index 0000000000..425d02b7ca --- /dev/null +++ b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx @@ -0,0 +1,281 @@ +import { + render, screen, waitFor, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import LmsApiService from '../../../../data/services/LmsApiService'; +import SettingsApiCredentialsTab from '../index'; +import { + API_CLIENT_DOCUMENTATION, HELP_CENTER_API_GUIDE, API_TERMS_OF_SERVICE, HELP_CENTER_LINK, +} from '../../data/constants'; + +jest.mock('../../../../data/services/LmsApiService', () => ({ + fetchAPICredentials: jest.fn(), + createNewAPICredentials: jest.fn(), + regenerateAPICredentials: jest.fn(), +})); + +const name = "edx's Enterprise Credentials"; +const clientId = 'y0TCvOEvvIs6ll95irirzCJ5EaF0RnSbBIIXuNJE'; +const clientSecret = '1G896sVeT67jtjHO6FNd5qFqayZPIV7BtnW01P8zaAd4mDfmBVVVsUP33u'; +const updated = '2023-07-28T04:28:20.909550Z'; +const redirectUris = 'www.customercourses.edx.com, www.customercourses.edx.stage.com'; + +const data = { + name, + client_id: clientId, + client_secret: clientSecret, + redirect_uris: redirectUris, + updated, +}; +const regenerationData = { + ...data, + redirect_uris: redirectUris, +}; +const copiedData = { + ...data, + api_client_documentation: API_CLIENT_DOCUMENTATION, +}; + +describe('API Credentials Tab', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + const basicProps = { + enterpriseId: 'test-enterprise-uuid', + }; + + const enterpriseId = 'test-enterprise-uuid'; + + test('renders zero state page when having no api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + const mockCreateFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockFetchFn.mockRejectedValue(); + mockCreateFn.mockResolvedValue(); + + render( + + + , + ); + expect(screen.getByText('API credentials')).toBeInTheDocument(); + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + expect(screen.getByText("You don't have API credentials yet.")).toBeInTheDocument(); + expect(screen.queryByText('Help Center: EdX Enterprise API Guide')).toBeInTheDocument(); + const helpLink = screen.getByText('Help Center: EdX Enterprise API Guide'); + expect(helpLink.getAttribute('href')).toBe(HELP_CENTER_API_GUIDE); + const serviceLink = screen.getByText('edX API terms of service'); + expect(serviceLink.getAttribute('href')).toBe(API_TERMS_OF_SERVICE); + + expect(screen.getByText('Generate API Credentials').toBeInTheDocument); + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => expect(mockCreateFn).toHaveBeenCalled()); + }); + test('renders api credentials page when having existing api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data }); + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + await waitFor(() => expect(screen.getByText(name)).toBeInTheDocument); + + expect(screen.getByText(name).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client documentation: ${API_CLIENT_DOCUMENTATION}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Last generated on: ${updated}` }).toBeInTheDocument); + const link = screen.getByText('contact Enterprise Customer Support.'); + expect(link.getAttribute('href')).toBe(HELP_CENTER_LINK); + }); + test('renders error stage while creating new api credentials through clicking generation button', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockRejectedValue(); + const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockCreatFn.mockRejectedValue(); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => { expect(mockCreatFn).toHaveBeenCalled(); }); + expect( + screen.getByText( + 'Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.', + ), + ).toBeInTheDocument(); + }); + test('renders api credentials page after successfully creating api credentials through clicking generation button', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockRejectedValue(); + const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockCreatFn.mockResolvedValue({ data }); + const writeText = jest.fn(); + Object.assign(navigator, { + clipboard: { + writeText, + }, + }); + const jsonString = JSON.stringify(copiedData); + navigator.clipboard.writeText.mockResolvedValue(jsonString); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => expect(mockCreatFn).toHaveBeenCalled()); + expect(screen.getByText('API credentials successfully generated')).toBeInTheDocument(); + const closeButton = screen.getByLabelText('Close'); + userEvent.click(closeButton); + await waitFor(() => { + expect(screen.queryByText('API credentials successfully generated')).not.toBeInTheDocument(); + }); + + expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client documentation: ${API_CLIENT_DOCUMENTATION}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Last generated on: ${updated}` }).toBeInTheDocument); + + const copyBtn = screen.getByText('Copy credentials to clipboard'); + userEvent.click(copyBtn); + await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith(jsonString)); + await waitFor(() => expect(screen.getByText('Copied Successfully')).toBeInTheDocument()); + }); + test('renders error message when failing to copying api credentials to clipboard', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockRejectedValue(); + const mockCreatFn = jest.spyOn(LmsApiService, 'createNewAPICredentials'); + mockCreatFn.mockResolvedValue({ data }); + const writeText = jest.fn(); + Object.assign(navigator, { + clipboard: { + writeText, + }, + }); + const jsonString = JSON.stringify(copiedData); + navigator.clipboard.writeText.mockRejectedValue(); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + + userEvent.click(screen.getByText('Generate API Credentials')); + await waitFor(() => expect(mockCreatFn).toHaveBeenCalled()); + const copyBtn = screen.getByText('Copy credentials to clipboard'); + userEvent.click(copyBtn); + await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith(jsonString)); + await waitFor(() => expect(screen.getByText('Cannot copied to the clipboard')).toBeInTheDocument()); + }); + test('renders api credentials page after successfully regenerating api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data }); + const mockPatchFn = jest.spyOn(LmsApiService, 'regenerateAPICredentials'); + mockPatchFn.mockResolvedValue({ data: regenerationData }); + + render( + + + , + ); + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + const input = screen.getByTestId('form-control'); + expect(input).toHaveValue(''); + userEvent.type(input, redirectUris); + await waitFor(() => expect(input).toHaveValue(redirectUris)); + const button = screen.getByText('Regenerate API Credentials'); + userEvent.click(button); + + await waitFor(() => expect(screen.getByText('Regenerate API credentials?')).toBeInTheDocument()); + const confirmedButton = screen.getByText('Regenerate'); + userEvent.click(confirmedButton); + await waitFor(() => { + expect(mockPatchFn).toHaveBeenCalledWith(redirectUris, enterpriseId); + }); + expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `API client documentation: ${API_CLIENT_DOCUMENTATION}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Last generated on: ${updated}` }).toBeInTheDocument); + expect(screen.queryByText('Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.')) + .not.toBeInTheDocument(); + }); + test('renders error state when failing to regenerating api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data }); + const mockPatchFn = jest.spyOn(LmsApiService, 'regenerateAPICredentials'); + mockPatchFn.mockRejectedValue(); + + render( + + + , + ); + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + const input = screen.getByTestId('form-control'); + expect(input).toHaveValue(''); + userEvent.type(input, redirectUris); + await waitFor(() => expect(input).toHaveValue(redirectUris)); + const button = screen.getByText('Regenerate API Credentials'); + userEvent.click(button); + + await waitFor(() => expect(screen.getByText('Regenerate API credentials?')).toBeInTheDocument()); + const confirmedButton = screen.getByText('Regenerate'); + userEvent.click(confirmedButton); + await waitFor(() => { + expect(mockPatchFn).toHaveBeenCalledWith(redirectUris, enterpriseId); + }); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByText('Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.')) + .toBeInTheDocument(); + }); + test('renders api credentials when canceling regenerating api credentials', async () => { + const mockFetchFn = jest.spyOn(LmsApiService, 'fetchAPICredentials'); + mockFetchFn.mockResolvedValue({ data }); + const mockPatchFn = jest.spyOn(LmsApiService, 'regenerateAPICredentials'); + mockPatchFn.mockResolvedValue({ data: regenerationData }); + + render( + + + , + ); + + await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); + const input = screen.getByTestId('form-control'); + expect(input).toHaveValue(''); + userEvent.type(input, redirectUris); + await waitFor(() => expect(input).toHaveValue(redirectUris)); + const button = screen.getByText('Regenerate API Credentials'); + userEvent.click(button); + + await waitFor(() => expect(screen.getByText('Regenerate API credentials?')).toBeInTheDocument()); + const cancelButton = screen.getByText('Cancel'); + userEvent.click(cancelButton); + await waitFor(() => { + expect(mockPatchFn).not.toHaveBeenCalledWith(redirectUris, enterpriseId); + }); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + }); +}); diff --git a/src/components/settings/SettingsLMSTab/index.jsx b/src/components/settings/SettingsLMSTab/index.jsx index 54c3c3fa1d..1da30d6a09 100644 --- a/src/components/settings/SettingsLMSTab/index.jsx +++ b/src/components/settings/SettingsLMSTab/index.jsx @@ -7,11 +7,12 @@ import PropTypes from 'prop-types'; import { camelCaseObject } from '@edx/frontend-platform/utils'; import { - Alert, Button, Hyperlink, Toast, Skeleton, useToggle, + Alert, Button, Toast, Skeleton, useToggle, } from '@edx/paragon'; import { Add, Info } from '@edx/paragon/icons'; import { logError } from '@edx/frontend-platform/logging'; +import HelpCenterButton from '../HelpCenterButton'; import { camelCaseDictArray, getChannelMap } from '../../../utils'; import LMSConfigPage from './LMSConfigPage'; import ExistingLMSCardDeck from './ExistingLMSCardDeck'; @@ -149,13 +150,9 @@ const SettingsLMSTab = ({ return (

Learning Platform Integrations - + Help Center: Integrations - +
{!configsLoading && !config && ( + {showCancelButton && } + {showBackButton && } {nextButtonConfig && ( + + +

+ 2. Launch a new window and upload the XML file to the list of + authorized SAML Service Providers on your Identity Provider's portal or website. +

+
+

Return to this window and check the box once complete

+ + + I have authorized edX as a Service Provider + + +); + +export default SSOConfigAuthorizeStep; diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx new file mode 100644 index 0000000000..50cf5b5ba2 --- /dev/null +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + Form, Container, +} from '@edx/paragon'; + +import ValidatedFormControl from '../../../forms/ValidatedFormControl'; + +const SSOConfigConfigureStep = () => { + const renderBaseFields = () => ( + <> +

Enter user attributes

+

+ Please enter the SAML user attributes from your Identity Provider. + All attributes are space and case sensitive. +

+ + + + + + + + + + + + + + + + + ); + const renderSAPFields = () => ( + <> +

Enable learner account auto-registration

+ + + + + + + + + + + + + + + + + ); + + return ( + + + +
+

Enter integration details

+

Set display name

+ + + + + {/* TODO: Render SAP fields selectively once logic is in place */} + {renderBaseFields()} + {renderSAPFields()} +
+
+ ); +}; + +export default SSOConfigConfigureStep; diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfirmStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfirmStep.tsx new file mode 100644 index 0000000000..f43bdeeb71 --- /dev/null +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfirmStep.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { + Alert, Hyperlink, OverlayTrigger, Popover, +} from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; + +const IncognitoPopover = () => ( + + + Steps to open a new window in incognito mode (also known as private mode) + may vary based on the browser you are using. + Review your browser's help documentation as needed. + + + )} + > + incognito window + +); + +const SSOConfigConfirmStep = () => ( + <> +

Wait for SSO configuration confirmation

+ +

Action required from email

+ Great news! You have completed the configuration steps, edX is actively configuring your SSO connection. + You will receive an email within about five minutes when the configuration is complete. + The email will include instructions for testing. +
+
+

What to expect:

+
    +
  • SSO configuration confirmation email.
  • +
      +
    • Testing instructions involve copying and pasting a custom URL into an
    • +
    • A link back to the SSO Settings page
    • +
    +
+
+

+ Select the "Finish" button below or close this form via the + "X" in the upper right corner while you wait for your + configuration email. Your SSO testing status will display on the following SSO settings screen. +

+ +); + +export default SSOConfigConfirmStep; diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx new file mode 100644 index 0000000000..4b66625d11 --- /dev/null +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Container, Dropzone, Form } from '@edx/paragon'; + +import ValidatedFormRadio from '../../../forms/ValidatedFormRadio'; +import ValidatedFormControl from '../../../forms/ValidatedFormControl'; +import { FormContext, useFormContext } from '../../../forms/FormContext'; + +const SSOConfigConnectStep = () => { + const fiveGbInBytes = 5368709120; + const ssoIdpOptions = [ + ['Microsoft Azure Active Directory (Azure AD)', 'azure_ad'], + ['Google Workspace', 'google_workspace'], + ['Okta', 'okta'], + ['OneLogin', 'one_login'], + ['SAP SuccessFactors', 'sap_success_factors'], + ['Other', 'other'], + ]; + const idpConnectOptions = [ + ['Enter identity Provider Metadata URL', 'idp_metadata_url'], + ['Upload Identity Provider Metadata XML file', 'idp_metadata_xml'], + ]; + + const { + formFields, + }: FormContext = useFormContext(); + const showUrlEntry = formFields?.idpConnectOption === 'idp_metadata_url'; + const showXmlUpload = formFields?.idpConnectOption === 'idp_metadata_xml'; + + // TODO: Store uploaded XML data + const onUploadXml = () => null; + + return ( + + +
+ +

Let's get started

+ What is your organization's SSO Identity Provider? + + + + +

Connect edX to your Identity Provider

+ Select a method to connect edX to your Identity Provider + + + + + {showUrlEntry && ( + + + + )} + + {showXmlUpload + && ( + + )} + +
+ ); +}; + +export default SSOConfigConnectStep; diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx index 03372c4fd5..4cc7cecc93 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx @@ -1,6 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { Provider } from 'react-redux'; import NewSSOConfigForm from '../NewSSOConfigForm'; @@ -13,6 +14,7 @@ import { INVALID_ODATA_API_TIMEOUT_INTERVAL, INVALID_SAPSF_OAUTH_ROOT_URL, INVALID_API_ROOT_URL, } from '../../data/constants'; import { features } from '../../../../config'; +import { getButtonElement } from '../../../test/testUtils'; jest.mock('../data/actions'); jest.mock('../../utils'); @@ -69,6 +71,17 @@ const contextValue = { setRefreshBool: jest.fn(), }; +const setupNewSSOStepper = () => { + features.AUTH0_SELF_SERVICE_INTEGRATION = true; + return render( + + + + + , + ); +}; + describe('SAML Config Tab', () => { afterEach(() => { features.AUTH0_SELF_SERVICE_INTEGRATION = false; @@ -309,15 +322,113 @@ describe('SAML Config Tab', () => { expect(screen.getByText('Next')).not.toBeDisabled(); }, []); }); - test('show new SSO stepper placeholder when feature flag enabled', async () => { - // Setup - features.AUTH0_SELF_SERVICE_INTEGRATION = true; - contextValue.ssoState.currentStep = 'idp'; - render( - - - , - ); + test('navigate through new sso workflow skeleton', async () => { + setupNewSSOStepper(); + // Connect Step + await waitFor(() => { + expect(getButtonElement('Next')).toBeInTheDocument(); + }, []); + expect(screen.queryByText('New SSO integration')).toBeInTheDocument(); + expect(screen.queryByText('Connect')).toBeInTheDocument(); + expect(screen.queryByText('Let\'s get started')).toBeInTheDocument(); + userEvent.click(getButtonElement('Next')); + + // Configure Step + await waitFor(() => { + expect(getButtonElement('Configure')).toBeInTheDocument(); + }, []); + expect(screen.queryByText('Enter integration details')).toBeInTheDocument(); + userEvent.click(getButtonElement('Configure')); + + // Authorize Step + await waitFor(() => { + expect(getButtonElement('Next')).toBeInTheDocument(); + }, []); + expect(screen.queryByText('Authorize edX as a Service Provider')).toBeInTheDocument(); + userEvent.click(getButtonElement('Next')); + + // Confirm and Test Step + await waitFor(() => { + expect(getButtonElement('Finish')).toBeInTheDocument(); + }, []); + expect(screen.queryByText('Wait for SSO configuration confirmation')).toBeInTheDocument(); + }); + test('show correct metadata entry based on selection', async () => { + setupNewSSOStepper(); + await waitFor(() => { + expect(getButtonElement('Next')).toBeInTheDocument(); + }, []); + + const enterUrlText = 'Find the URL in your Identity Provider portal or website.'; + const uploadXmlText = 'Drag and drop your file here or click to upload.'; + + // Verify metadata selectors are hidden initially + expect(screen.queryByText(enterUrlText)).not.toBeInTheDocument(); + expect(screen.queryByText(uploadXmlText)).not.toBeInTheDocument(); + + // Verify metadata selectors appear with their respective selections + userEvent.click(screen.getByText('Enter identity Provider Metadata URL')); + await waitFor(() => { + expect(screen.queryByText(enterUrlText)).toBeInTheDocument(); + }, []); + expect(screen.queryByText(uploadXmlText)).not.toBeInTheDocument(); + + userEvent.click(screen.getByText('Upload Identity Provider Metadata XML file')); + await waitFor(() => { + expect(screen.queryByText(uploadXmlText)).toBeInTheDocument(); + }, []); + expect(screen.queryByText(enterUrlText)).not.toBeInTheDocument(); + }); + test('back button shown on pages after first page', async () => { + const getBackButton = () => getButtonElement('Back'); + setupNewSSOStepper(); + // Connect Step + await waitFor(() => { + expect(getButtonElement('Next')).toBeInTheDocument(); + }, []); + expect(screen.queryByRole('button', { name: 'Back' })).not.toBeInTheDocument(); + userEvent.click(getButtonElement('Next')); + + // Configure Step + await waitFor(() => { + expect(getButtonElement('Configure')).toBeInTheDocument(); + }, []); + expect(getBackButton()).toBeInTheDocument(); + userEvent.click(getButtonElement('Configure')); + + // Authorize Step + await waitFor(() => { + expect(getButtonElement('Next')).toBeInTheDocument(); + }, []); + expect(getBackButton()).toBeInTheDocument(); + userEvent.click(getButtonElement('Next')); + + // Back from Confirm and Test Step + await waitFor(() => { + expect(getButtonElement('Finish')).toBeInTheDocument(); + }, []); + userEvent.click(getBackButton()); + await waitFor(() => { + expect(screen.queryByText('Authorize edX as a Service Provider')).toBeInTheDocument(); + }, []); + }); + test('cancel out of new SSO workflow', async () => { + setupNewSSOStepper(); + // Connect Step + await waitFor(() => { + expect(getButtonElement('Cancel')).toBeInTheDocument(); + }, []); + userEvent.click(getButtonElement('Cancel')); + await waitFor(() => { + expect(getButtonElement('Cancel')).toBeInTheDocument(); + }, []); + + await waitFor(() => { + const exitButton = getButtonElement('Exit without saving'); + expect(exitButton).toBeInTheDocument(); + userEvent.click(exitButton); + }, []); + await waitFor(() => { expect( screen.queryByText( @@ -325,7 +436,6 @@ describe('SAML Config Tab', () => { + ' to allow quick access to your organization\'s learning catalog.', ), ).toBeInTheDocument(); - expect(screen.queryByText('Next')).not.toBeInTheDocument(); }, []); }); test('idp step fetches and displays existing idp data fields', async () => { diff --git a/src/components/test/testUtils.jsx b/src/components/test/testUtils.jsx index 539cd0a3ad..b5ac1bf34e 100644 --- a/src/components/test/testUtils.jsx +++ b/src/components/test/testUtils.jsx @@ -3,7 +3,7 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; export function renderWithRouter( ui, @@ -30,3 +30,5 @@ export function findElementWithText(container, type, text) { const elements = container.querySelectorAll(type); return [...elements].find((elem) => elem.innerHTML.includes(text)); } + +export const getButtonElement = (buttonText) => screen.getByRole('button', { name: buttonText }); From ed98264e122e47f80edaf6186a1e80c81892a1b7 Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Fri, 22 Sep 2023 08:57:57 -0600 Subject: [PATCH 026/124] fix: Bug bash fixes (#1029) * fix: Bug bash fixes * fix: test fixes --- .../APICredentialsPage.jsx | 5 +- .../SettingsApiCredentialsTab/CopyButton.jsx | 2 + .../ZeroStateCard.jsx | 21 ++++++--- .../SettingsApiCredentialsTab/index.jsx | 46 ++++++++++--------- .../tests/SettingsAPICredentialsPage.test.jsx | 46 ++++++++++--------- 5 files changed, 69 insertions(+), 51 deletions(-) diff --git a/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx b/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx index f86dc4aaaf..37e10d2222 100644 --- a/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/APICredentialsPage.jsx @@ -8,7 +8,7 @@ import CopyButton from './CopyButton'; import { API_CLIENT_DOCUMENTATION, HELP_CENTER_LINK } from '../data/constants'; const APICredentialsPage = ({ data, setData }) => { - const [formValue, setFormValue] = useState(''); + const [formValue, setFormValue] = useState(data?.redirect_uris); const handleFormChange = (e) => { setFormValue(e.target.value); }; @@ -48,7 +48,7 @@ const APICredentialsPage = ({ data, setData }) => {

-
+

Redirect URIs (optional)

If you need additional redirect URIs, add them below and regenerate your API credentials. @@ -77,6 +77,7 @@ const APICredentialsPage = ({ data, setData }) => { contact Enterprise Customer Support. diff --git a/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx b/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx index eb3477e047..42e73bbc13 100644 --- a/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/CopyButton.jsx @@ -12,6 +12,8 @@ const CopyButton = ({ data }) => { const handleCopyLink = async () => { try { + const newData = data; + ['user', 'id'].forEach(prop => delete newData[prop]); const jsonString = JSON.stringify(data); await navigator.clipboard.writeText(jsonString); } catch (error) { diff --git a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx index a9e37a0b8f..32a6d60f35 100644 --- a/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/ZeroStateCard.jsx @@ -1,4 +1,4 @@ -import React, { useState, useContext } from 'react'; +import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { @@ -41,24 +41,33 @@ const ZeroStateCard = ({ setShowToast, setData }) => {

You don't have API credentials yet.

{ !displayFailureAlert && (

- This page allows you to generate API credentials to send to  + This page allows you to generate API credentials to send to your developers so they can work on integration projects. If you believe you are seeing this page in error,  contact Enterprise Customer Support.

)}

- edX for Business API credentials credentials will provide access  - to the following edX API endpoints: reporting dashboard, dashboard, and catalog administration. + edX for Business API credentials will provide access to the following + edX API endpoints: reporting dashboard, dashboard, and catalog administration.

- By clicking the button below, you and your organization accept the {'\n'} - edX API terms of service. + By clicking the button below, you and your organization accept the  + + edX API terms of service. +

diff --git a/src/components/settings/SettingsApiCredentialsTab/index.jsx b/src/components/settings/SettingsApiCredentialsTab/index.jsx index 2cdef4d762..31e0da4c2d 100644 --- a/src/components/settings/SettingsApiCredentialsTab/index.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/index.jsx @@ -34,34 +34,36 @@ const SettingsApiCredentialsTab = ({ }, [enterpriseId]); return ( - - - - { hasRegenerationError && } - -

API credentials

- - - Help Center: EdX Enterprise API Guide - -
-
- {!data ? ( - - ) : ()} -
-
- { showToast && ( +

+ + + + { hasRegenerationError && } + +

API credentials

+ + + Help Center: EdX Enterprise API Guide + + +
+ {!data ? ( + + ) : ()} +
+
+ { showToast && ( setShowToast(false)} show={showToast} > API credentials successfully generated - )} - - - + )} + + + +

); }; SettingsApiCredentialsTab.propTypes = { diff --git a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx index 425d02b7ca..1c751835d1 100644 --- a/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx +++ b/src/components/settings/SettingsApiCredentialsTab/tests/SettingsAPICredentialsPage.test.jsx @@ -20,18 +20,19 @@ const name = "edx's Enterprise Credentials"; const clientId = 'y0TCvOEvvIs6ll95irirzCJ5EaF0RnSbBIIXuNJE'; const clientSecret = '1G896sVeT67jtjHO6FNd5qFqayZPIV7BtnW01P8zaAd4mDfmBVVVsUP33u'; const updated = '2023-07-28T04:28:20.909550Z'; -const redirectUris = 'www.customercourses.edx.com, www.customercourses.edx.stage.com'; +const redirectUri1 = 'www.customercourses.edx.com'; +const redirectUri2 = 'www.customercourses.edx.com'; const data = { name, client_id: clientId, client_secret: clientSecret, - redirect_uris: redirectUris, + redirect_uris: redirectUri1, updated, }; const regenerationData = { ...data, - redirect_uris: redirectUris, + redirect_uris: redirectUri1, }; const copiedData = { ...data, @@ -66,7 +67,7 @@ describe('API Credentials Tab', () => { expect(screen.queryByText('Help Center: EdX Enterprise API Guide')).toBeInTheDocument(); const helpLink = screen.getByText('Help Center: EdX Enterprise API Guide'); expect(helpLink.getAttribute('href')).toBe(HELP_CENTER_API_GUIDE); - const serviceLink = screen.getByText('edX API terms of service'); + const serviceLink = screen.getByText('edX API terms of service.'); expect(serviceLink.getAttribute('href')).toBe(API_TERMS_OF_SERVICE); expect(screen.getByText('Generate API Credentials').toBeInTheDocument); @@ -86,7 +87,7 @@ describe('API Credentials Tab', () => { await waitFor(() => expect(screen.getByText(name)).toBeInTheDocument); expect(screen.getByText(name).toBeInTheDocument); - expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUri1}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client documentation: ${API_CLIENT_DOCUMENTATION}` }).toBeInTheDocument); @@ -147,7 +148,7 @@ describe('API Credentials Tab', () => { }); expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); - expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUri1}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client documentation: ${API_CLIENT_DOCUMENTATION}` }).toBeInTheDocument); @@ -200,9 +201,11 @@ describe('API Credentials Tab', () => { ); await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); const input = screen.getByTestId('form-control'); - expect(input).toHaveValue(''); - userEvent.type(input, redirectUris); - await waitFor(() => expect(input).toHaveValue(redirectUris)); + expect(input).toHaveValue(redirectUri1); + userEvent.clear(input); + userEvent.type(input, redirectUri2); + + await waitFor(() => expect(input).toHaveValue(redirectUri2)); const button = screen.getByText('Regenerate API Credentials'); userEvent.click(button); @@ -210,10 +213,13 @@ describe('API Credentials Tab', () => { const confirmedButton = screen.getByText('Regenerate'); userEvent.click(confirmedButton); await waitFor(() => { - expect(mockPatchFn).toHaveBeenCalledWith(redirectUris, enterpriseId); + expect(mockPatchFn).toHaveBeenCalledWith(redirectUri2, enterpriseId); + }); + await waitFor(() => { + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUri2}` }).toBeInTheDocument); }); expect(screen.getByRole('heading', { name: `Application name: ${name}` }).toBeInTheDocument); - expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUri2}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client ID: ${clientId}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client secret: ${clientSecret}` }).toBeInTheDocument); expect(screen.getByRole('heading', { name: `API client documentation: ${API_CLIENT_DOCUMENTATION}` }).toBeInTheDocument); @@ -234,9 +240,9 @@ describe('API Credentials Tab', () => { ); await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); const input = screen.getByTestId('form-control'); - expect(input).toHaveValue(''); - userEvent.type(input, redirectUris); - await waitFor(() => expect(input).toHaveValue(redirectUris)); + expect(input).toHaveValue(redirectUri1); + userEvent.type(input, ` ${redirectUri2}`); + await waitFor(() => expect(input).toHaveValue(`${redirectUri1} ${redirectUri2}`)); const button = screen.getByText('Regenerate API Credentials'); userEvent.click(button); @@ -244,9 +250,9 @@ describe('API Credentials Tab', () => { const confirmedButton = screen.getByText('Regenerate'); userEvent.click(confirmedButton); await waitFor(() => { - expect(mockPatchFn).toHaveBeenCalledWith(redirectUris, enterpriseId); + expect(mockPatchFn).toHaveBeenCalledWith(`${redirectUri1} ${redirectUri2}`, enterpriseId); }); - expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUri1}` }).toBeInTheDocument); expect(screen.getByText('Something went wrong while generating your credentials. Please try again. If the issue continues, contact Enterprise Customer Support.')) .toBeInTheDocument(); }); @@ -264,9 +270,7 @@ describe('API Credentials Tab', () => { await waitFor(() => expect(mockFetchFn).toHaveBeenCalled()); const input = screen.getByTestId('form-control'); - expect(input).toHaveValue(''); - userEvent.type(input, redirectUris); - await waitFor(() => expect(input).toHaveValue(redirectUris)); + expect(input).toHaveValue(redirectUri1); const button = screen.getByText('Regenerate API Credentials'); userEvent.click(button); @@ -274,8 +278,8 @@ describe('API Credentials Tab', () => { const cancelButton = screen.getByText('Cancel'); userEvent.click(cancelButton); await waitFor(() => { - expect(mockPatchFn).not.toHaveBeenCalledWith(redirectUris, enterpriseId); + expect(mockPatchFn).not.toHaveBeenCalledWith(redirectUri1, enterpriseId); }); - expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUris}` }).toBeInTheDocument); + expect(screen.getByRole('heading', { name: `Allowed URIs: ${redirectUri1}` }).toBeInTheDocument); }); }); From ce8c6d776456279210a52abfe731662a0d309d68 Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Mon, 18 Sep 2023 20:37:33 +0000 Subject: [PATCH 027/124] feat: enterprise sso orchestrator record api client --- .env.development | 1 + src/data/services/LmsApiService.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/.env.development b/.env.development index 03858d1ff4..9e9f84cf22 100644 --- a/.env.development +++ b/.env.development @@ -51,3 +51,4 @@ MAINTENANCE_ALERT_START_TIMESTAMP='' USE_API_CACHE='true' SUBSCRIPTION_LPR='true' PLOTLY_SERVER_URL='http://localhost:8050' +AUTH0_SELF_SERVICE_INTEGRATION='true' diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js index 4cff4834d1..2ed35e8274 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -39,6 +39,35 @@ class LmsApiService { static apiCredentialsUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise-customer-api-credentials/`; + static enterpriseSsoOrchestrationUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise_customer_sso_configuration/`; + + static fetchEnterpriseSsoOrchestrationRecord(configurationUuid) { + const enterpriseSsoOrchestrationFetchUrl = `${LmsApiService.enterpriseSsoOrchestrationUrl}${configurationUuid}`; + return LmsApiService.apiClient().get(enterpriseSsoOrchestrationFetchUrl); + } + + static listEnterpriseSsoOrchestration(enterpriseCustomerUuid) { + const enterpriseSsoOrchestrationListUrl = `${LmsApiService.enterpriseSsoOrchestrationUrl}`; + if (enterpriseCustomerUuid) { + return LmsApiService.apiClient().get(`${enterpriseSsoOrchestrationListUrl}?enterprise_customer=${enterpriseCustomerUuid}`); + } + return LmsApiService.apiClient().get(enterpriseSsoOrchestrationListUrl); + } + + static createEnterpriseSsoOrchestrationRecord(formData) { + return LmsApiService.apiClient().post(LmsApiService.enterpriseSsoOrchestrationUrl, formData); + } + + static updateEnterpriseSsoOrchestrationRecord(formData, configurationUuid) { + const enterpriseSsoOrchestrationUpdateUrl = `${LmsApiService.enterpriseSsoOrchestrationUrl}${configurationUuid}`; + return LmsApiService.apiClient().put(enterpriseSsoOrchestrationUpdateUrl, formData); + } + + static deleteEnterpriseSsoOrchestrationRecord(configurationUuid) { + const enterpriseSsoOrchestrationDeleteUrl = `${LmsApiService.enterpriseSsoOrchestrationUrl}${configurationUuid}`; + return LmsApiService.apiClient().delete(enterpriseSsoOrchestrationDeleteUrl); + } + static fetchEnterpriseList(options) { const queryParams = new URLSearchParams({ page: 1, From 9ed5ab37b5da85fa1c97189b681d71e8be3583b1 Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Tue, 19 Sep 2023 18:06:58 +0500 Subject: [PATCH 028/124] feat: add support for optional salesforce ids in csv upload --- src/components/InviteLearnersModal/index.jsx | 6 +- src/data/validation/email.js | 75 ++++++++++++-- src/data/validation/email.test.js | 102 +++++++++++++++++++ 3 files changed, 173 insertions(+), 10 deletions(-) diff --git a/src/components/InviteLearnersModal/index.jsx b/src/components/InviteLearnersModal/index.jsx index 89ee1e62d6..29c0f75e89 100644 --- a/src/components/InviteLearnersModal/index.jsx +++ b/src/components/InviteLearnersModal/index.jsx @@ -10,7 +10,7 @@ import { camelCaseObject } from '@edx/frontend-platform/utils'; import emailTemplate from './emailTemplate'; import TextAreaAutoSize from '../TextAreaAutoSize'; import FileInput from '../FileInput'; -import { returnValidatedEmails, validateEmailAddrTemplateForm } from '../../data/validation/email'; +import { extractSalesforceIds, returnValidatedEmails, validateEmailAddrTemplateForm } from '../../data/validation/email'; import { normalizeFileUpload } from '../../utils'; class InviteLearnersModal extends React.Component { @@ -74,6 +74,10 @@ class InviteLearnersModal extends React.Component { }; options.user_emails = returnValidatedEmails(formData); + const SFIDs = extractSalesforceIds(formData, options.user_emails); + if (SFIDs) { + options.user_sfids = SFIDs; + } /* eslint-disable no-underscore-dangle */ return addLicensesForUsers(options, subscriptionUUID) diff --git a/src/data/validation/email.js b/src/data/validation/email.js index c39edc6585..a7f757d8a7 100644 --- a/src/data/validation/email.js +++ b/src/data/validation/email.js @@ -82,6 +82,33 @@ const validateEmailAddresses = (emails) => { return result; }; +// Each row in textarea or csv can contain email plus an optional salesforce id +// Email and salesforce id will be separated by comma. This function will read +// each row, split it by comma and then return an object with three properties: +// textEmails: All emails extracted from textarea +// csvEmails: All emails extracted from CSV +// allEmails: Concatenation of `textEmails` and `csvEmails` +const extractEmails = (formData) => { + let textEmails = []; + let csvEmails = []; + let allEmails = []; + + if (formData[EMAIL_ADDRESS_TEXT_FORM_DATA] && formData[EMAIL_ADDRESS_TEXT_FORM_DATA].length) { + textEmails = formData[EMAIL_ADDRESS_TEXT_FORM_DATA].split(/\r\n|\n/).map(item => item.split(',')[0]); + } + if (formData[EMAIL_ADDRESS_CSV_FORM_DATA] && formData[EMAIL_ADDRESS_CSV_FORM_DATA].length) { + csvEmails = formData[EMAIL_ADDRESS_CSV_FORM_DATA].map(item => item.split(',')[0]); + } + + allEmails = [...textEmails, ...csvEmails]; + + return { + textEmails, + csvEmails, + allEmails, + }; +}; + const validateEmailAddressesFields = (formData) => { // Validate that email address fields contain valid-looking emails. // Expects Redux form data @@ -90,11 +117,11 @@ const validateEmailAddressesFields = (formData) => { _error: [], }; - const textAreaEmails = formData[EMAIL_ADDRESS_TEXT_FORM_DATA] && formData[EMAIL_ADDRESS_TEXT_FORM_DATA].split(/\r\n|\n/); - const csvEmails = formData[EMAIL_ADDRESS_CSV_FORM_DATA]; + const { csvEmails, textEmails } = extractEmails(formData); + const emails = textEmails.length ? textEmails : csvEmails; let { invalidEmailIndices, - } = validateEmailAddresses(textAreaEmails || csvEmails); + } = validateEmailAddresses(emails); // 1 is added to every index to fix off-by-one error in messages shown to the user. invalidEmailIndices = invalidEmailIndices.map(i => i + 1); @@ -105,7 +132,7 @@ const validateEmailAddressesFields = (formData) => { ${invalidEmailIndices.length !== 0 ? `and ${lastEmail}` : `${lastEmail}`} \ is invalid. Please try again.`; - errorsDict[textAreaEmails ? EMAIL_ADDRESS_TEXT_FORM_DATA : EMAIL_ADDRESS_CSV_FORM_DATA] = message; + errorsDict[textEmails.length ? EMAIL_ADDRESS_TEXT_FORM_DATA : EMAIL_ADDRESS_CSV_FORM_DATA] = message; errorsDict._error.push(message); } @@ -155,19 +182,49 @@ const returnValidatedEmails = (formData) => { if (errorsDict._error.length > 0) { throw new SubmissionError(errorsDict); } - let emails = []; + + const emails = _.union(extractEmails(formData).allEmails); // Dedup emails + return validateEmailAddresses(emails).validEmails; +}; + +// Combine all the rows from textarea and CSV and then make a map of email to salesforce id +const getSalesforceIdsByEmail = (formData) => { + const rows = []; + const allRecords = {}; + if (formData[EMAIL_ADDRESS_TEXT_FORM_DATA] && formData[EMAIL_ADDRESS_TEXT_FORM_DATA].length) { - emails.push(...formData[EMAIL_ADDRESS_TEXT_FORM_DATA].split(/\r\n|\n/)); + rows.push(...formData[EMAIL_ADDRESS_TEXT_FORM_DATA].split(/\r\n|\n/)); } + if (formData[EMAIL_ADDRESS_CSV_FORM_DATA] && formData[EMAIL_ADDRESS_CSV_FORM_DATA].length) { - emails.push(...formData[EMAIL_ADDRESS_CSV_FORM_DATA]); + rows.push(...formData[EMAIL_ADDRESS_CSV_FORM_DATA]); } - emails = _.union(emails); // Dedup emails - return validateEmailAddresses(emails).validEmails; + + rows.forEach((row) => { + const [email, salesforceId] = row.split(',').map(item => item.trim()); + allRecords[email] = salesforceId; + }); + + return allRecords; +}; + +// Extract salesforce ids for all validated emails +const extractSalesforceIds = (formData, userEmails) => { + const salesforceIdsByEmail = getSalesforceIdsByEmail(formData); + const ids = []; + + userEmails.forEach((email) => { + ids.push(salesforceIdsByEmail[email]); + }); + + // check if `ids` array contain non-empty, not-null values + const noIdsPresent = ids.every(item => !item); + return noIdsPresent ? undefined : ids; }; /* eslint-enable no-underscore-dangle */ export { + extractSalesforceIds, validateEmailAddresses, validateEmailAddressesFields, validateEmailTemplateForm, diff --git a/src/data/validation/email.test.js b/src/data/validation/email.test.js index bb32bec662..4cc46ac214 100644 --- a/src/data/validation/email.test.js +++ b/src/data/validation/email.test.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import { + extractSalesforceIds, validateEmailAddresses, validateEmailAddressesFields, validateEmailTemplateFields, @@ -233,4 +234,105 @@ describe('email validation', () => { ); }); }); + + describe('validate emails and ids extraction', () => { + it('extracted correct emails and ids from textarea and csv', () => { + const formData = new FormData(); + formData[EMAIL_ADDRESS_TEXT_FORM_DATA] = [ + 'abc@example.com,000000000000ABCABC', + 'asdf@example.com,', + 'zzz@example.com,000000000000XYZXYZ', + ].join('\n'); + formData[EMAIL_ADDRESS_CSV_FORM_DATA] = [ + 'one@example.com,000000000000YYYYYY', + 'two@example.com,000000000000ZZZZZZ', + 'three@example.com,000000000000ABCDDD', + 'wow@example.com,', + 'abc@example.com,000000000000ABCABC', + 'ama@example.com', + ]; + const userEmails = [ + 'abc@example.com', + 'asdf@example.com', + 'zzz@example.com', + 'one@example.com', + 'two@example.com', + 'three@example.com', + 'wow@example.com', + 'ama@example.com', + ]; + + const salesforceIds = extractSalesforceIds(formData, userEmails); + expect(salesforceIds).toEqual([ + '000000000000ABCABC', + '', + '000000000000XYZXYZ', + '000000000000YYYYYY', + '000000000000ZZZZZZ', + '000000000000ABCDDD', + '', + undefined, + ]); + expect(userEmails.length).toEqual(salesforceIds.length); + }); + + it('extracted correct emails and ids from textarea only', () => { + const formData = new FormData(); + formData[EMAIL_ADDRESS_TEXT_FORM_DATA] = [ + 'aaa@example.com,000000000000ABCABC', + 'bbb@example.com,', + 'ccc@example.com,000000000000XYZXYZ', + 'ddd@example.com', + ].join('\n'); + + const userEmails = returnValidatedEmails(formData); + expect(userEmails).toEqual(['aaa@example.com', 'bbb@example.com', 'ccc@example.com', 'ddd@example.com']); + const salesforceIds = extractSalesforceIds(formData, userEmails); + expect(salesforceIds).toEqual([ + '000000000000ABCABC', + '', + '000000000000XYZXYZ', + undefined, + ]); + expect(userEmails.length).toEqual(salesforceIds.length); + }); + + it('extracted correct emails and ids from csv only', () => { + const formData = new FormData(); + formData[EMAIL_ADDRESS_CSV_FORM_DATA] = [ + 'eee@example.com,000000000000YYYYYY', + 'fff@example.com,', + 'ggg@example.com,000000000000ZZZZZZ', + 'hhh@example.com', + ]; + + const userEmails = returnValidatedEmails(formData); + expect(userEmails).toEqual(['eee@example.com', 'fff@example.com', 'ggg@example.com', 'hhh@example.com']); + const salesforceIds = extractSalesforceIds(formData, userEmails); + expect(salesforceIds).toEqual([ + '000000000000YYYYYY', + '', + '000000000000ZZZZZZ', + undefined, + ]); + expect(userEmails.length).toEqual(salesforceIds.length); + }); + + it('returns no salesforce ids for emails only', () => { + const formData = new FormData(); + formData[EMAIL_ADDRESS_TEXT_FORM_DATA] = [ + 'abc@example.com', + 'asdf@example.com,', + ].join('\n'); + formData[EMAIL_ADDRESS_CSV_FORM_DATA] = [ + 'one@example.com,', + 'two@example.com', + ]; + + const userEmails = returnValidatedEmails(formData); + expect(userEmails).toEqual(['abc@example.com', 'asdf@example.com', 'one@example.com', 'two@example.com']); + const salesforceIds = extractSalesforceIds(formData, userEmails); + expect(salesforceIds).toEqual(undefined); + }); + }); }); From 7874784a7bee6c422177357dd0f2908e922cd662 Mon Sep 17 00:00:00 2001 From: jajjibhai008 Date: Fri, 15 Sep 2023 19:03:16 +0500 Subject: [PATCH 029/124] feat: show all enterprise budgets regardless of plan and route correctly fix: full page refresh issue when clicking 'Budgets', added test coverage and fixed lint issues feat: show all enterprise budgets regardless of plan and route correctly fix: moved LCM routes to separate file fix: resolve spacing and repetitions issue fix: resolve spacing and repetitions issue fix: improved test coverage refactor: improve code efficiency and readability fix: incorporate adam's suggestions and nits fix: incorporated adam's feedback fix: incorporate adam's feedback --- .../EnterpriseApp/EnterpriseAppRoutes.jsx | 8 +- .../EnterpriseApp/data/constants.js | 11 ++ .../EnterpriseSubsidiesContext/data/hooks.js | 65 ++++--- .../data/tests/BudgetDetailPage.test.jsx | 138 ++++++++++++++ .../data/tests/hooks.test.js | 111 ++++++----- .../BudgetCard-V2.jsx | 174 ++++-------------- .../BudgetDetailPage.jsx | 85 +++++++++ .../MultipleBudgetsPage.jsx | 17 +- .../MultipleBudgetsPicker.jsx | 29 +-- .../SubBudgetCard.jsx | 103 +++++++++++ .../learner-credit-management/data/hooks.js | 13 +- .../data/tests/hooks.test.js | 8 + .../data/tests/utils.test.js | 72 +++++++- .../learner-credit-management/data/utils.js | 45 ++++- .../learner-credit-management/index.js | 3 - .../learner-credit-management/index.jsx | 28 +++ .../tests/BudgetCard.test.jsx | 124 ++++++++++--- 17 files changed, 769 insertions(+), 265 deletions(-) create mode 100644 src/components/EnterpriseSubsidiesContext/data/tests/BudgetDetailPage.test.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailPage.jsx create mode 100644 src/components/learner-credit-management/SubBudgetCard.jsx delete mode 100644 src/components/learner-credit-management/index.js create mode 100644 src/components/learner-credit-management/index.jsx diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx index b5853281d5..e46986950f 100644 --- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx +++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx @@ -13,9 +13,9 @@ import { SubscriptionManagementPage } from '../subscriptions'; import { PlotlyAnalyticsPage } from '../PlotlyAnalytics'; import { ROUTE_NAMES } from './data/constants'; import BulkEnrollmentResultsDownloadPage from '../BulkEnrollmentResultsDownloadPage'; -import LearnerCreditManagement from '../learner-credit-management'; import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; import ContentHighlights from '../ContentHighlights'; +import LearnerCreditManagementRoutes from '../learner-credit-management'; const EnterpriseAppRoutes = ({ baseUrl, @@ -98,10 +98,8 @@ const EnterpriseAppRoutes = ({ /> {canManageLearnerCredit && ( - )} diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js index 5c881fefab..6feaac51f7 100644 --- a/src/components/EnterpriseApp/data/constants.js +++ b/src/components/EnterpriseApp/data/constants.js @@ -13,3 +13,14 @@ export const ROUTE_NAMES = { subscriptionManagement: 'subscriptions', contentHighlights: 'content-highlights', }; + +export const BUDGET_STATUSES = { + active: 'Active', + expired: 'Expired', + upcoming: 'Upcoming', +}; + +export const BUDGET_TYPES = { + ecommerce: 'ecommerce', + subsidy: 'subsidy', +}; diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index d699098cd0..6e9b040167 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -10,6 +10,7 @@ import { camelCaseObject } from '@edx/frontend-platform/utils'; import EcommerceApiService from '../../../data/services/EcommerceApiService'; import LicenseManagerApiService from '../../../data/services/LicenseManagerAPIService'; import SubsidyApiService from '../../../data/services/EnterpriseSubsidyApiService'; +import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; export const useEnterpriseOffers = ({ enablePortalLearnerCreditManagementScreen, enterpriseId }) => { const [offers, setOffers] = useState([]); @@ -25,42 +26,40 @@ export const useEnterpriseOffers = ({ enablePortalLearnerCreditManagementScreen, try { const [enterpriseSubsidyResponse, ecommerceApiResponse] = await Promise.all([ SubsidyApiService.getSubsidyByCustomerUUID(enterpriseId, { subsidyType: 'learner_credit' }), - EcommerceApiService.fetchEnterpriseOffers({ - isCurrent: true, - }), + EcommerceApiService.fetchEnterpriseOffers(), ]); - // If there are no subsidies in enterprise, fall back to the e-commerce API. - let { results } = camelCaseObject(enterpriseSubsidyResponse.data); - let source = 'subsidyApi'; + // We have to consider both type of offers active and inactive. - if (results.length === 0) { - results = camelCaseObject(ecommerceApiResponse.data.results); - source = 'ecommerceApi'; - } - let activeSubsidyFound = false; - if (results.length !== 0) { - let subsidy = results[0]; - const offerData = []; - let activeSubsidyData = {}; - for (let i = 0; i < results.length; i++) { - subsidy = results[i]; - activeSubsidyFound = source === 'ecommerceApi' - ? subsidy.isCurrent - : subsidy.isActive; - if (activeSubsidyFound === true) { - activeSubsidyData = { - id: subsidy.uuid || subsidy.id, - name: subsidy.title || subsidy.displayName, - start: subsidy.activeDatetime || subsidy.startDatetime, - end: subsidy.expirationDatetime || subsidy.endDatetime, - isCurrent: activeSubsidyFound, - }; - offerData.push(activeSubsidyData); - setCanManageLearnerCredit(true); - } - } - setOffers(offerData); + const enterpriseSubsidyResults = camelCaseObject(enterpriseSubsidyResponse.data).results; + const ecommerceOffersResults = camelCaseObject(ecommerceApiResponse.data.results); + + const offerData = []; + + enterpriseSubsidyResults.forEach((result) => { + offerData.push({ + source: BUDGET_TYPES.subsidy, + id: result.uuid, + name: result.title, + start: result.activeDatetime, + end: result.expirationDatetime, + isCurrent: result.isActive, + }); + }); + + ecommerceOffersResults.forEach((result) => { + offerData.push({ + source: BUDGET_TYPES.ecommerce, + id: (result.id).toString(), + name: result.displayName, + start: result.startDatetime, + end: result.endDatetime, + isCurrent: result.isCurrent, + }); + }); + setOffers(offerData); + if (offerData.length > 0) { + setCanManageLearnerCredit(true); } } catch (error) { logError(error); diff --git a/src/components/EnterpriseSubsidiesContext/data/tests/BudgetDetailPage.test.jsx b/src/components/EnterpriseSubsidiesContext/data/tests/BudgetDetailPage.test.jsx new file mode 100644 index 0000000000..483263fdce --- /dev/null +++ b/src/components/EnterpriseSubsidiesContext/data/tests/BudgetDetailPage.test.jsx @@ -0,0 +1,138 @@ +/* eslint-disable react/prop-types */ +import React from 'react'; +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import { + screen, + render, +} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { MemoryRouter } from 'react-router-dom'; +import BudgetDetailPage from '../../../learner-credit-management/BudgetDetailPage'; +import { useOfferSummary, useOfferRedemptions } from '../../../learner-credit-management/data/hooks'; +import { EXEC_ED_OFFER_TYPE } from '../../../learner-credit-management/data/constants'; +import { EnterpriseSubsidiesContext } from '../..'; + +jest.mock('../../../learner-credit-management/data/hooks'); + +useOfferSummary.mockReturnValue({ + isLoading: false, + offerSummary: null, +}); +useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: { + itemCount: 0, + pageCount: 0, + results: [], + }, + fetchOfferRedemptions: jest.fn(), +}); + +const mockStore = configureMockStore([thunk]); +const getMockStore = store => mockStore(store); +const enterpriseId = 'test-enterprise'; +const enterpriseUUID = '1234'; +const initialStore = { + portalConfiguration: { + enterpriseId, + enterpriseSlug: enterpriseId, + + }, +}; +const store = getMockStore({ ...initialStore }); + +const mockEnterpriseOfferId = '123'; + +const mockOfferDisplayName = 'Test Enterprise Offer'; +const mockOfferSummary = { + totalFunds: 5000, + redeemedFunds: 200, + remainingFunds: 4800, + percentUtilized: 0.04, + offerType: EXEC_ED_OFFER_TYPE, +}; + +const defaultEnterpriseSubsidiesContextValue = { + isLoading: false, +}; + +const BudgetDetailPageWrapper = ({ + enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue, + ...rest +}) => ( + + + + + + + + + + +); + +describe('', () => { + describe('with enterprise offer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays table on clicking view budget', async () => { + const mockOffer = { + id: mockEnterpriseOfferId, + name: mockOfferDisplayName, + start: '2022-01-01', + end: '2023-01-01', + }; + useOfferSummary.mockReturnValue({ + isLoading: false, + offerSummary: mockOfferSummary, + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: { + itemCount: 0, + pageCount: 0, + results: [], + }, + fetchOfferRedemptions: jest.fn(), + }); + render(); + expect(screen.getByText('Learner Credit Management')); + expect(screen.getByText('Overview')); + expect(screen.getByText('No results found')); + }); + + it('displays loading message while loading data', async () => { + useOfferSummary.mockReturnValue({ + isLoading: true, + offerSummary: null, + }); + useOfferRedemptions.mockReturnValue({ + isLoading: true, + offerRedemptions: { + itemCount: 0, + pageCount: 0, + results: [], + }, + fetchOfferRedemptions: jest.fn(), + }); + + render(); + + expect(screen.getByText('loading')); + }); + }); +}); diff --git a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js index adf8580b52..ec1c5466af 100644 --- a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js +++ b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js @@ -4,6 +4,7 @@ import { useCoupons, useCustomerAgreement, useEnterpriseOffers } from '../hooks' import EcommerceApiService from '../../../../data/services/EcommerceApiService'; import LicenseManagerApiService from '../../../../data/services/LicenseManagerAPIService'; import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; +import { BUDGET_TYPES } from '../../../EnterpriseApp/data/constants'; jest.mock('@edx/frontend-platform/config', () => ({ getConfig: jest.fn(() => ({ @@ -51,6 +52,7 @@ describe('useEnterpriseOffers', () => { start: '2021-05-15T19:56:09Z', end: '2100-05-15T19:56:09Z', isCurrent: true, + source: BUDGET_TYPES.ecommerce, }]; SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ @@ -69,9 +71,7 @@ describe('useEnterpriseOffers', () => { await waitForNextUpdate(); - expect(EcommerceApiService.fetchEnterpriseOffers).toHaveBeenCalledWith({ - isCurrent: true, - }); + expect(EcommerceApiService.fetchEnterpriseOffers).toHaveBeenCalled(); expect(result.current).toEqual({ offers: mockOffers, isLoading: false, @@ -80,25 +80,35 @@ describe('useEnterpriseOffers', () => { }); it('should fetch enterprise offers for the enterprise when data available in enterprise-subsidy', async () => { - const mockOffers = [ + const mockEnterpriseSubsidyResponse = [ { - id: 'offer-id', - name: 'offer-name', - start: '2021-05-15T19:56:09Z', - end: '2100-05-15T19:56:09Z', - isCurrent: true, + uuid: 'offer-id', + title: 'offer-name', + activeDatetime: '2021-05-15T19:56:09Z', + expirationDatetime: '2100-05-15T19:56:09Z', + isActive: true, }, ]; - const mockSubsidyServiceResponse = [{ - uuid: 'offer-id', - title: 'offer-name', - active_datetime: '2021-05-15T19:56:09Z', - expiration_datetime: '2100-05-15T19:56:09Z', - is_active: true, - }]; + + const mockEcommerceResponse = [ + { + id: 'uuid', + display_name: 'offer-name', + start_datetime: '2021-05-15T19:56:09Z', + end_datetime: '2100-05-15T19:56:09Z', + is_current: true, + }, + ]; + SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ data: { - results: mockSubsidyServiceResponse, + results: mockEnterpriseSubsidyResponse, + }, + }); + + EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ + data: { + results: mockEcommerceResponse, }, }); @@ -113,36 +123,39 @@ describe('useEnterpriseOffers', () => { TEST_ENTERPRISE_UUID, { subsidyType: 'learner_credit' }, ); + + const expectedOffers = [ + { + id: 'offer-id', + name: 'offer-name', + start: '2021-05-15T19:56:09Z', + end: '2100-05-15T19:56:09Z', + isCurrent: true, + source: BUDGET_TYPES.subsidy, + }, + { + id: 'uuid', + name: 'offer-name', + start: '2021-05-15T19:56:09Z', + end: '2100-05-15T19:56:09Z', + isCurrent: true, + source: BUDGET_TYPES.ecommerce, + }, + ]; + expect(result.current).toEqual({ - offers: mockOffers, + offers: expectedOffers, isLoading: false, canManageLearnerCredit: true, }); }); it('should set canManageLearnerCredit to false if active enterprise offer or subsidy not found', async () => { - const mockOffers = [{ subsidyUuid: 'offer-1' }, { subsidyUuid: 'offer-2' }]; - const mockSubsidyServiceResponse = [ - { - uuid: 'offer-1', - title: 'offer-name', - active_datetime: '2005-05-15T19:56:09Z', - expiration_datetime: '2006-05-15T19:56:09Z', - is_active: false, - }, - { - uuid: 'offer-2', - title: 'offer-name-2', - active_datetime: '2006-05-15T19:56:09Z', - expiration_datetime: '2007-05-15T19:56:09Z', - is_active: false, - }, - ]; - const mockOfferData = []; + const mockSubsidyServiceResponse = []; EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ data: { - results: mockOffers, + results: [], }, }); SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ @@ -162,15 +175,22 @@ describe('useEnterpriseOffers', () => { TEST_ENTERPRISE_UUID, { subsidyType: 'learner_credit' }, ); + + const hasActiveOffersOrSubsidies = mockSubsidyServiceResponse.some(offer => offer.is_active); + let canManageLearnerCredit = false; + + if (hasActiveOffersOrSubsidies) { + canManageLearnerCredit = true; + } + expect(result.current).toEqual({ - offers: mockOfferData, + offers: [], isLoading: false, - canManageLearnerCredit: false, + canManageLearnerCredit, }); }); it('should return the active enterprise offer or subsidy when multiple available', async () => { - const mockOffers = [{ subsidyUuid: 'offer-1' }, { subsidyUuid: 'offer-2' }]; const mockSubsidyServiceResponse = [ { uuid: 'offer-1', @@ -188,18 +208,27 @@ describe('useEnterpriseOffers', () => { }, ]; const mockOfferData = [ + { + id: 'offer-1', + name: 'offer-name', + start: '2005-05-15T19:56:09Z', + end: '2006-05-15T19:56:09Z', + isCurrent: false, + source: BUDGET_TYPES.subsidy, + }, { id: 'offer-2', name: 'offer-name-2', start: '2006-05-15T19:56:09Z', end: '2099-05-15T19:56:09Z', isCurrent: true, + source: BUDGET_TYPES.subsidy, }, ]; EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ data: { - results: mockOffers, + results: [], }, }); SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx index b39b9297d9..e6780a61db 100644 --- a/src/components/learner-credit-management/BudgetCard-V2.jsx +++ b/src/components/learner-credit-management/BudgetCard-V2.jsx @@ -1,25 +1,17 @@ -import React, { useState } from 'react'; +/* eslint-disable react/jsx-no-useless-fragment */ +/* eslint-disable no-nested-ternary */ +import React from 'react'; import PropTypes from 'prop-types'; -import dayjs from 'dayjs'; -import { - Card, - Button, - Stack, - Row, - Col, - Breadcrumb, -} from '@edx/paragon'; - -import { useOfferRedemptions, useOfferSummary } from './data/hooks'; -import LearnerCreditAggregateCards from './LearnerCreditAggregateCards'; -import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; -import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { useOfferSummary } from './data/hooks'; +import SubBudgetCard from './SubBudgetCard'; +import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; const BudgetCard = ({ offer, enterpriseUUID, enterpriseSlug, - enableLearnerPortal, + offerType, + displayName, }) => { const { start, @@ -31,124 +23,37 @@ const BudgetCard = ({ offerSummary, } = useOfferSummary(enterpriseUUID, offer); - const { - isLoading: isLoadingOfferRedemptions, - offerRedemptions, - fetchOfferRedemptions, - } = useOfferRedemptions(enterpriseUUID, offer?.id); - const [detailPage, setDetailPage] = useState(false); - const [activeLabel, setActiveLabel] = useState(''); - const links = [ - { label: 'Budgets', url: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}` }, - ]; - const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); - const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); - const navigateToBudgetRedemptions = (budgetType) => { - setDetailPage(true); - links.push({ label: budgetType, url: `/${enterpriseSlug}/admin/learner-credit` }); - setActiveLabel(budgetType); - }; - - const renderActions = (budgetType) => ( - - ); - - const renderCardHeader = (budgetType) => { - const subtitle = ( -
- - {formattedStartDate} - {formattedExpirationDate} - -
- ); - - return ( - - {renderActions(budgetType)} -
- )} - /> - ); - }; - - const renderCardSection = (available, spent) => ( - - - - Available - {available} - - - Spent - {spent} - - - - ); - - const renderCardAggregate = () => ( -
- -
- ); - return ( - - - - - - - {!detailPage - ? ( - <> - {renderCardAggregate()} -

Budgets

- - - - {renderCardHeader('Overview')} - {renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFunds)} - - - - - ) - : ( - - )} -
+ <> + {offerType === BUDGET_TYPES.ecommerce ? ( + + ) : ( + <> + {offerSummary?.budgetsSummary?.map((budget) => ( + + ))} + + )} + ); }; @@ -161,7 +66,8 @@ BudgetCard.propTypes = { }).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, - enableLearnerPortal: PropTypes.bool.isRequired, + offerType: PropTypes.string.isRequired, + displayName: PropTypes.string, }; export default BudgetCard; diff --git a/src/components/learner-credit-management/BudgetDetailPage.jsx b/src/components/learner-credit-management/BudgetDetailPage.jsx new file mode 100644 index 0000000000..ad90b5ae89 --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailPage.jsx @@ -0,0 +1,85 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { + Row, + Col, + Breadcrumb, + Container, +} from '@edx/paragon'; +import { connect } from 'react-redux'; +import { Helmet } from 'react-helmet'; +import { useParams, Link } from 'react-router-dom'; +import Hero from '../Hero'; + +import LoadingMessage from '../LoadingMessage'; +import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; + +import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; +import { useOfferRedemptions } from './data/hooks'; +import { isUUID } from './data/utils'; +import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; + +const PAGE_TITLE = 'Learner Credit Management'; + +const BudgetDetailPage = ({ + enterpriseUUID, + enterpriseSlug, + enableLearnerPortal, +}) => { + const { budgetId } = useParams(); + const enterpriseOfferId = isUUID(budgetId) ? null : budgetId; + const subsidyAccessPolicyId = isUUID(budgetId) ? budgetId : null; + + const { isLoading } = useContext(EnterpriseSubsidiesContext); + const { + isLoading: isLoadingOfferRedemptions, + offerRedemptions, + fetchOfferRedemptions, + } = useOfferRedemptions(enterpriseUUID, enterpriseOfferId, subsidyAccessPolicyId); + if (isLoading) { + return ; + } + const links = [ + { label: 'Budgets', to: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}` }, + ]; + return ( + <> + + + + + + + + + + + + ); +}; + +const mapStateToProps = state => ({ + enterpriseUUID: state.portalConfiguration.enterpriseId, + enterpriseSlug: state.portalConfiguration.enterpriseSlug, + enableLearnerPortal: state.portalConfiguration.enableLearnerPortal, +}); + +BudgetDetailPage.propTypes = { + enterpriseUUID: PropTypes.string.isRequired, + enterpriseSlug: PropTypes.string.isRequired, + enableLearnerPortal: PropTypes.bool.isRequired, +}; + +export default connect(mapStateToProps)(BudgetDetailPage); diff --git a/src/components/learner-credit-management/MultipleBudgetsPage.jsx b/src/components/learner-credit-management/MultipleBudgetsPage.jsx index 3df18c465a..fe12ab2719 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPage.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPage.jsx @@ -6,6 +6,7 @@ import { Col, Card, Hyperlink, + Container, } from '@edx/paragon'; import { connect } from 'react-redux'; import { Helmet } from 'react-helmet'; @@ -17,7 +18,7 @@ import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; import { configuration } from '../../config'; -const PAGE_TITLE = 'Learner Credit'; +const PAGE_TITLE = 'Learner Credit Management'; const MultipleBudgetsPage = ({ enterpriseUUID, @@ -63,12 +64,14 @@ const MultipleBudgetsPage = ({ <> - + + + ); }; diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index 4c3da2d0ce..db6f178f2a 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -14,18 +14,25 @@ const MultipleBudgetsPicker = ({ enterpriseSlug, enableLearnerPortal, }) => ( - + - - {offers.map(offer => ( - - ))} +

Budgets

+
+ + + + {offers.map(offer => ( + + ))} +
diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx new file mode 100644 index 0000000000..d3360cea43 --- /dev/null +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -0,0 +1,103 @@ +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; +import { + Card, + Button, + Row, + Col, +} from '@edx/paragon'; + +import { BUDGET_STATUSES, ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { formatPrice, getBudgetStatus } from './data/utils'; + +const SubBudgetCard = ({ + id, + start, + end, + available, + spent, + displayName, + enterpriseSlug, + isLoading, +}) => { + const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); + const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); + const budgetStatus = getBudgetStatus(start, end); + + const renderActions = (budgetId) => ( + + ); + + const renderCardHeader = (budgetType, budgetId) => { + const subtitle = ( +
+ + {formattedStartDate} - {formattedExpirationDate} + +
+ ); + + return ( + + ); + }; + + const renderCardSection = (availableBalance, spentBalance) => ( + + + + Available + {formatPrice(availableBalance)} + + + Spent + {formatPrice(spentBalance)} + + + + ); + + return ( + + + {renderCardHeader(displayName || 'Overview', id)} + {budgetStatus !== BUDGET_STATUSES.upcoming && renderCardSection(available, spent)} + + + ); +}; + +SubBudgetCard.propTypes = { + enterpriseSlug: PropTypes.string.isRequired, + id: PropTypes.string, + start: PropTypes.string, + end: PropTypes.string, + spent: PropTypes.number, + isLoading: PropTypes.bool, + available: PropTypes.number, + displayName: PropTypes.string, +}; + +export default SubBudgetCard; diff --git a/src/components/learner-credit-management/data/hooks.js b/src/components/learner-credit-management/data/hooks.js index 585970c35e..31577f36a7 100644 --- a/src/components/learner-credit-management/data/hooks.js +++ b/src/components/learner-credit-management/data/hooks.js @@ -74,7 +74,7 @@ const applyFiltersToOptions = (filters, options) => { } }; -export const useOfferRedemptions = (enterpriseUUID, offerId) => { +export const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) => { const shouldTrackFetchEvents = useRef(false); const [isLoading, setIsLoading] = useState(true); const [offerRedemptions, setOfferRedemptions] = useState({ @@ -90,9 +90,14 @@ export const useOfferRedemptions = (enterpriseUUID, offerId) => { const options = { page: args.pageIndex + 1, // `DataTable` uses zero-indexed array pageSize: args.pageSize, - offerId, ignoreNullCourseListPrice: true, }; + if (budgetId !== null) { + options.budgetId = budgetId; + } + if (offerId !== null) { + options.offerId = offerId; + } if (args.sortBy?.length > 0) { applySortByToOptions(args.sortBy, options); } @@ -129,10 +134,10 @@ export const useOfferRedemptions = (enterpriseUUID, offerId) => { setIsLoading(false); } }; - if (offerId) { + if (offerId || budgetId) { fetch(); } - }, [enterpriseUUID, offerId, shouldTrackFetchEvents]); + }, [enterpriseUUID, offerId, budgetId, shouldTrackFetchEvents]); const debouncedFetchOfferRedemptions = useMemo(() => debounce(fetchOfferRedemptions, 300), [fetchOfferRedemptions]); diff --git a/src/components/learner-credit-management/data/tests/hooks.test.js b/src/components/learner-credit-management/data/tests/hooks.test.js index 8ab61bce2f..38ebaeaafd 100644 --- a/src/components/learner-credit-management/data/tests/hooks.test.js +++ b/src/components/learner-credit-management/data/tests/hooks.test.js @@ -72,6 +72,9 @@ describe('useOfferSummary', () => { redeemedFundsOcm: NaN, remainingFunds: 4800, percentUtilized: 0.04, + offerId: 1, + budgetsSummary: [], + offerType: undefined, }; expect(result.current).toEqual({ offerSummary: expectedResult, @@ -83,9 +86,11 @@ describe('useOfferSummary', () => { describe('useOfferRedemptions', () => { it('should fetch enrollment/redemptions metadata for enterprise offer', async () => { EnterpriseDataApiService.fetchCourseEnrollments.mockResolvedValueOnce({ data: mockOfferEnrollmentsResponse }); + const budgetId = 'test-budget-id'; const { result, waitForNextUpdate } = renderHook(() => useOfferRedemptions( TEST_ENTERPRISE_UUID, mockEnterpriseOffer.id, + budgetId, )); expect(result.current).toMatchObject({ @@ -119,6 +124,7 @@ describe('useOfferRedemptions', () => { ordering: '-enrollment_date', // default sort order searchAll: mockOfferEnrollments[0].user_email, ignoreNullCourseListPrice: true, + budgetId, }; expect(EnterpriseDataApiService.fetchCourseEnrollments).toHaveBeenCalledWith( TEST_ENTERPRISE_UUID, @@ -133,5 +139,7 @@ describe('useOfferRedemptions', () => { isLoading: false, fetchOfferRedemptions: expect.any(Function), }); + + expect(expectedApiOptions.budgetId).toBe(budgetId); }); }); diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js index 33902d40fe..96061c8af7 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -1,4 +1,4 @@ -import { transformOfferSummary } from '../utils'; +import { transformOfferSummary, getBudgetStatus } from '../utils'; import { EXEC_ED_OFFER_TYPE } from '../constants'; describe('transformOfferSummary', () => { @@ -23,6 +23,8 @@ describe('transformOfferSummary', () => { remainingFunds: 0.0, percentUtilized: 1.0, offerType: EXEC_ED_OFFER_TYPE, + budgetsSummary: [], + offerId: undefined, }); }); @@ -33,6 +35,8 @@ describe('transformOfferSummary', () => { remainingBalance: null, percentOfOfferSpent: null, offerType: 'Site', + offerId: '123', + budgetsSummary: [], }; expect(transformOfferSummary(offerSummary)).toEqual({ @@ -41,6 +45,72 @@ describe('transformOfferSummary', () => { remainingFunds: null, percentUtilized: null, offerType: 'Site', + redeemedFundsExecEd: undefined, + redeemedFundsOcm: undefined, + offerId: '123', + budgetsSummary: [], }); }); + + it('should handle when budgetsSummary is provided', () => { + const offerSummary = { + maxDiscount: 1000, + amountOfOfferSpent: 500, + remainingBalance: 500, + percentOfOfferSpent: 0.5, + offerType: 'Site', + offerId: '123', + budgets: [ + { + id: 123, + start: '2022-01-01', + end: '2022-01-01', + available: 200, + spent: 100, + enterpriseSlug: 'test-enterprise', + }], + }; + + expect(transformOfferSummary(offerSummary)).toEqual({ + totalFunds: 1000, + redeemedFunds: 500, + remainingFunds: 500, + percentUtilized: 0.5, + offerType: 'Site', + redeemedFundsExecEd: NaN, + redeemedFundsOcm: NaN, + offerId: '123', + budgetsSummary: [{ + id: 123, + start: '2022-01-01', + end: '2022-01-01', + available: 200, + spent: 100, + enterpriseSlug: 'test-enterprise', + }], + }); + }); +}); + +describe('getBudgetStatus', () => { + it('should return "upcoming" when the current date is before the start date', () => { + const startDateStr = '2023-09-30'; + const endDateStr = '2023-10-30'; + const result = getBudgetStatus(startDateStr, endDateStr); + expect(result).toEqual('Upcoming'); + }); + + it('should return "active" when the current date is between the start and end dates', () => { + const startDateStr = '2023-09-01'; + const endDateStr = '2023-09-30'; + const result = getBudgetStatus(startDateStr, endDateStr); + expect(result).toEqual('Active'); + }); + + it('should return "expired" when the current date is after the end date', () => { + const startDateStr = '2023-08-01'; + const endDateStr = '2023-08-31'; + const result = getBudgetStatus(startDateStr, endDateStr); + expect(result).toEqual('Expired'); + }); }); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 65524c1346..5bd64d257d 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -3,6 +3,7 @@ import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, } from './constants'; +import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; /** * Transforms offer summary from API for display in the UI, guarding * against bad data (e.g., accounting for refunds). @@ -12,6 +13,20 @@ import { */ export const transformOfferSummary = (offerSummary) => { if (!offerSummary) { return null; } + const budgetsSummary = []; + if (offerSummary?.budgets) { + const budgets = offerSummary?.budgets; + for (let i = 0; i < budgets.length; i++) { + const redeemedFunds = budgets[i].amountOfPolicySpent && parseFloat(budgets[i].amountOfPolicySpent); + const remainingFunds = budgets[i].remainingBalance && parseFloat(budgets[i].remainingBalance); + const updatedBudgetDetail = { + redeemedFunds, + remainingFunds, + ...budgets[i], + }; + budgetsSummary.push(updatedBudgetDetail); + } + } const totalFunds = offerSummary.maxDiscount && parseFloat(offerSummary.maxDiscount); let redeemedFunds = offerSummary.amountOfOfferSpent && parseFloat(offerSummary.amountOfOfferSpent); @@ -38,7 +53,7 @@ export const transformOfferSummary = (offerSummary) => { percentUtilized = Math.min(percentUtilized, 1.0); } const { offerType } = offerSummary; - + const { offerId } = offerSummary; return { totalFunds, redeemedFunds, @@ -47,6 +62,8 @@ export const transformOfferSummary = (offerSummary) => { remainingFunds, percentUtilized, offerType, + offerId, + budgetsSummary, }; }; @@ -91,3 +108,29 @@ export const getProgressBarVariant = ({ percentUtilized, remainingFunds }) => { } return variant; }; + +// Utility function to check if the ID is a UUID +export const isUUID = (id) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id); + +// Utility function to check the budget status +export const getBudgetStatus = (startDateStr, endDateStr) => { + const currentDate = new Date(); + const startDate = new Date(startDateStr); + const endDate = new Date(endDateStr); + + if (currentDate < startDate) { + return BUDGET_STATUSES.upcoming; + } + if (currentDate >= startDate && currentDate <= endDate) { + return BUDGET_STATUSES.active; + } + return BUDGET_STATUSES.expired; +}; + +export const formatPrice = (price) => { + const USDollar = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }); + return USDollar.format(Math.abs(price)); +}; diff --git a/src/components/learner-credit-management/index.js b/src/components/learner-credit-management/index.js deleted file mode 100644 index 271f4453ed..0000000000 --- a/src/components/learner-credit-management/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import MultipleBudgetsPage from './MultipleBudgetsPage'; - -export default MultipleBudgetsPage; diff --git a/src/components/learner-credit-management/index.jsx b/src/components/learner-credit-management/index.jsx new file mode 100644 index 0000000000..2ce695b9a7 --- /dev/null +++ b/src/components/learner-credit-management/index.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Route } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import MultipleBudgetsPage from './MultipleBudgetsPage'; +import BudgetDetailPage from './BudgetDetailPage'; + +const LearnerCreditManagementRoutes = ({ baseUrl }) => ( + <> + + + + +); + +LearnerCreditManagementRoutes.propTypes = { + baseUrl: PropTypes.string.isRequired, +}; + +export default LearnerCreditManagementRoutes; diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 7d8f349bda..d8aa511a4a 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -1,21 +1,20 @@ /* eslint-disable react/prop-types */ import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; -import userEvent from '@testing-library/user-event'; import configureMockStore from 'redux-mock-store'; import dayjs from 'dayjs'; import { screen, render, - waitFor, } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import BudgetCard from '../BudgetCard-V2'; import { useOfferSummary, useOfferRedemptions } from '../data/hooks'; -import { EXEC_ED_OFFER_TYPE } from '../data/constants'; +import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; jest.mock('../data/hooks'); useOfferSummary.mockReturnValue({ @@ -47,20 +46,15 @@ const mockEnterpriseOfferId = '123'; const mockEnterpriseOfferEnrollmentId = 456; const mockOfferDisplayName = 'Test Enterprise Offer'; -const mockOfferSummary = { - totalFunds: 5000, - redeemedFunds: 200, - remainingFunds: 4800, - percentUtilized: 0.04, - offerType: EXEC_ED_OFFER_TYPE, -}; const BudgetCardWrapper = ({ ...rest }) => ( - - - - - + + + + + + + ); describe('', () => { @@ -88,6 +82,16 @@ describe('', () => { remainingFunds: 4800, percentUtilized: 0.04, offerType: 'Site', + budgetsSummary: [ + { + id: 123, + start: '2022-01-01', + end: '2022-01-01', + available: 200, + spent: 100, + enterpriseSlug: enterpriseId, + }, + ], }, }); useOfferRedemptions.mockReturnValue({ @@ -106,42 +110,112 @@ describe('', () => { />); expect(screen.getByText('Overview')); expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); - expect(screen.getByText(`$${mockOfferSummary.redeemedFunds.toLocaleString()}`)); const formattedString = `${dayjs(mockOffer.start).format('MMMM D, YYYY')} - ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; const elementsWithTestId = screen.getAllByTestId('offer-date'); const firstElementWithTestId = elementsWithTestId[0]; expect(firstElementWithTestId).toHaveTextContent(formattedString); }); - it('displays table on clicking view budget', async () => { + it('renders SubBudgetCard when offerType is ecommerce', () => { const mockOffer = { id: mockEnterpriseOfferId, name: mockOfferDisplayName, start: '2022-01-01', end: '2023-01-01', + offerType: BUDGET_TYPES.ecommerce, + }; + const mockOfferRedemption = { + created: '2022-02-01', + enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, }; useOfferSummary.mockReturnValue({ isLoading: false, - offerSummary: mockOfferSummary, + offerSummary: { + totalFunds: 5000, + redeemedFunds: 200, + remainingFunds: 4800, + percentUtilized: 0.04, + offerType: 'learner_credit', + budgetsSummary: [ + { + id: 123, + start: '2022-01-01', + end: '2022-01-01', + available: 200, + spent: 100, + enterpriseSlug: enterpriseId, + }, + ], + }, }); useOfferRedemptions.mockReturnValue({ isLoading: false, offerRedemptions: { - itemCount: 0, - pageCount: 0, - results: [], + results: [mockOfferRedemption], + itemCount: 1, + pageCount: 1, }, fetchOfferRedemptions: jest.fn(), }); + render(); - const elementsWithTestId = screen.getAllByTestId('view-budget'); - const firstElementWithTestId = elementsWithTestId[0]; - await waitFor(() => userEvent.click(firstElementWithTestId)); - expect(screen.getByText('No results found')); + + expect(screen.getByTestId('view-budget')).toBeInTheDocument(); + }); + + it('renders SubBudgetCard when offerType is not ecommerce', () => { + const mockOffer = { + id: mockEnterpriseOfferId, + name: mockOfferDisplayName, + start: '2022-01-01', + end: '2023-01-01', + offerType: 'otherOfferType', + }; + const mockOfferRedemption = { + created: '2022-02-01', + enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, + }; + useOfferSummary.mockReturnValue({ + isLoading: false, + offerSummary: { + totalFunds: 5000, + redeemedFunds: 200, + remainingFunds: 4800, + percentUtilized: 0.04, + offerType: 'learner_credit', + budgetsSummary: [ + { + id: 123, + start: '2022-01-01', + end: '2022-01-01', + available: 200, + spent: 100, + enterpriseSlug: enterpriseId, + }, + ], + }, + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: { + results: [mockOfferRedemption], + itemCount: 1, + pageCount: 1, + }, + fetchOfferRedemptions: jest.fn(), + }); + + render(); + + expect(screen.getByTestId('view-budget')).toBeInTheDocument(); }); }); }); From 9cc4dad8060c0724d6b9ae87b4fb19e2fba6e910 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Thu, 28 Sep 2023 08:59:17 -0400 Subject: [PATCH 030/124] fix: ensure highlights routes appear when learner credit is available (#1043) --- src/components/EnterpriseApp/EnterpriseAppRoutes.jsx | 7 ++++--- src/components/learner-credit-management/index.jsx | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx index e46986950f..07b10958cb 100644 --- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx +++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx @@ -98,8 +98,9 @@ const EnterpriseAppRoutes = ({ /> {canManageLearnerCredit && ( - )} @@ -110,7 +111,7 @@ const EnterpriseAppRoutes = ({ /> )} - + ); }; diff --git a/src/components/learner-credit-management/index.jsx b/src/components/learner-credit-management/index.jsx index 2ce695b9a7..e3b88632ff 100644 --- a/src/components/learner-credit-management/index.jsx +++ b/src/components/learner-credit-management/index.jsx @@ -1,28 +1,29 @@ import React from 'react'; import { Route } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; import MultipleBudgetsPage from './MultipleBudgetsPage'; import BudgetDetailPage from './BudgetDetailPage'; -const LearnerCreditManagementRoutes = ({ baseUrl }) => ( +const LearnerCreditManagementRoutes = ({ match }) => ( <> ); LearnerCreditManagementRoutes.propTypes = { - baseUrl: PropTypes.string.isRequired, + match: PropTypes.shape({ + path: PropTypes.string.isRequired, + }).isRequired, }; export default LearnerCreditManagementRoutes; From 47938adc92a02c7b4cb920cb42b7449d9477d7a6 Mon Sep 17 00:00:00 2001 From: Mashal Malik <107556986+Mashal-m@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:07:16 +0500 Subject: [PATCH 031/124] refactor: add @openedx in renovate automate configuration (#1045) * refactor: add @openedx in renovate automate configuration * refactor: updated util getBudgetStatus and respective test to resolve test failure --------- Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com> --- renovate.json | 2 +- .../learner-credit-management/data/tests/utils.test.js | 6 ++++-- src/components/learner-credit-management/data/utils.js | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/renovate.json b/renovate.json index b332af838c..8f6a3afef6 100644 --- a/renovate.json +++ b/renovate.json @@ -8,7 +8,7 @@ "rebaseStalePrs": true, "packageRules": [ { - "matchPackagePatterns": ["@edx"], + "matchPackagePatterns": ["@edx", "@openedx"], "matchUpdateTypes": ["minor", "patch"], "automerge": true } diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js index 96061c8af7..ab19a94d27 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -96,14 +96,16 @@ describe('getBudgetStatus', () => { it('should return "upcoming" when the current date is before the start date', () => { const startDateStr = '2023-09-30'; const endDateStr = '2023-10-30'; - const result = getBudgetStatus(startDateStr, endDateStr); + const currentDateStr = '2023-09-28'; + const result = getBudgetStatus(startDateStr, endDateStr, new Date(currentDateStr)); expect(result).toEqual('Upcoming'); }); it('should return "active" when the current date is between the start and end dates', () => { const startDateStr = '2023-09-01'; const endDateStr = '2023-09-30'; - const result = getBudgetStatus(startDateStr, endDateStr); + const currentDateStr = '2023-09-05'; + const result = getBudgetStatus(startDateStr, endDateStr, new Date(currentDateStr)); expect(result).toEqual('Active'); }); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 5bd64d257d..c42cb4039c 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -113,8 +113,7 @@ export const getProgressBarVariant = ({ percentUtilized, remainingFunds }) => { export const isUUID = (id) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id); // Utility function to check the budget status -export const getBudgetStatus = (startDateStr, endDateStr) => { - const currentDate = new Date(); +export const getBudgetStatus = (startDateStr, endDateStr, currentDate = new Date()) => { const startDate = new Date(startDateStr); const endDate = new Date(endDateStr); From 4d7cb88e249f6637c32e504aa3994234ef8794e4 Mon Sep 17 00:00:00 2001 From: jajjibhai008 Date: Mon, 2 Oct 2023 16:58:49 +0500 Subject: [PATCH 032/124] feat: label and order each budget card on lCM page --- .../EnterpriseApp/data/constants.js | 2 +- .../MultipleBudgetsPicker.jsx | 52 +++++++------ .../SubBudgetCard.jsx | 24 +++--- .../data/tests/utils.test.js | 77 +++++++++++++++---- .../learner-credit-management/data/utils.js | 54 ++++++++++++- .../tests/BudgetCard.test.jsx | 2 +- .../tests/MultipleBudgetsPage.test.jsx | 25 +++++- 7 files changed, 180 insertions(+), 56 deletions(-) diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js index 6feaac51f7..1773fd73e2 100644 --- a/src/components/EnterpriseApp/data/constants.js +++ b/src/components/EnterpriseApp/data/constants.js @@ -17,7 +17,7 @@ export const ROUTE_NAMES = { export const BUDGET_STATUSES = { active: 'Active', expired: 'Expired', - upcoming: 'Upcoming', + scheduled: 'Scheduled', }; export const BUDGET_TYPES = { diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index db6f178f2a..44407531c0 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -7,36 +7,40 @@ import { } from '@edx/paragon'; import BudgetCard from './BudgetCard-V2'; +import { orderOffers } from './data/utils'; const MultipleBudgetsPicker = ({ offers, enterpriseUUID, enterpriseSlug, enableLearnerPortal, -}) => ( - - -

Budgets

-
- - - - {offers.map(offer => ( - - ))} - - - -
-); +}) => { + const orderedOffers = orderOffers(offers); + return ( + + +

Budgets

+
+ + + + {orderedOffers?.map(offer => ( + + ))} + + + +
+ ); +}; MultipleBudgetsPicker.propTypes = { offers: PropTypes.arrayOf(PropTypes.shape()).isRequired, diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index d3360cea43..3841aebea3 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -6,6 +6,8 @@ import { Button, Row, Col, + Badge, + Stack, } from '@edx/paragon'; import { BUDGET_STATUSES, ROUTE_NAMES } from '../EnterpriseApp/data/constants'; @@ -21,9 +23,8 @@ const SubBudgetCard = ({ enterpriseSlug, isLoading, }) => { - const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); - const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); - const budgetStatus = getBudgetStatus(start, end); + const budgetLabel = getBudgetStatus(start, end); + const formattedDate = dayjs(budgetLabel?.date).format('MMMM D, YYYY'); const renderActions = (budgetId) => ( +
+ + + +); + +AnalyticsDetailCard.propTypes = { + onClose: PropTypes.func.isRequired, + isLoading: PropTypes.bool, + error: PropTypes.instanceOf(Error), + data: PropTypes.string, +}; + +const AIAnalyticsSummary = ({ enterpriseId, insights }) => { + const [summarizeCardIsOpen, showSummarizeCard, hideSummarizeCard] = useToggle(false); + const [trackProgressCardIsOpen, showTrackProgressCard, hideTrackProgressCard] = useToggle(false); + + const { data: analyticsSummary, isLoading, error } = useAIAnalyticsSummary(enterpriseId, insights); + + return ( + <> + + + + + {summarizeCardIsOpen && ( + hideSummarizeCard(true)} + isLoading={isLoading} + error={error} + /> + )} + {trackProgressCardIsOpen && ( + hideTrackProgressCard(true)} + isLoading={isLoading} + error={error} + /> + )} + + ); +}; + +const mapStateToProps = state => ({ + insights: state.dashboardInsights.insights, +}); + +AIAnalyticsSummary.propTypes = { + enterpriseId: PropTypes.string.isRequired, + insights: PropTypes.objectOf(PropTypes.shape), +}; + +export default connect(mapStateToProps)(AIAnalyticsSummary); diff --git a/src/components/Admin/AIAnalyticsSummary.test.jsx b/src/components/Admin/AIAnalyticsSummary.test.jsx new file mode 100644 index 0000000000..89b9a0c760 --- /dev/null +++ b/src/components/Admin/AIAnalyticsSummary.test.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import renderer from 'react-test-renderer'; +import configureMockStore from 'redux-mock-store'; +import { MemoryRouter } from 'react-router-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import thunk from 'redux-thunk'; +import AIAnalyticsSummary from './AIAnalyticsSummary'; + +const mockedInsights = { + learner_progress: { + enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9', + enterprise_customer_name: 'Microsoft Corporation', + active_subscription_plan: true, + assigned_licenses: 0, + activated_licenses: 0, + assigned_licenses_percentage: 0.0, + activated_licenses_percentage: 0.0, + active_enrollments: 1026, + at_risk_enrollment_less_than_one_hour: 26, + at_risk_enrollment_end_date_soon: 15, + at_risk_enrollment_dormant: 918, + created_at: '2023-10-02T03:24:17Z', + }, + learner_engagement: { + enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9', + enterprise_customer_name: 'Microsoft Corporation', + enrolls: 49, + enrolls_prior: 45, + passed: 2, + passed_prior: 0, + engage: 67, + engage_prior: 50, + hours: 62, + hours_prior: 49, + contract_end_date: '2022-06-13T00:00:00Z', + active_contract: false, + created_at: '2023-10-02T03:24:40Z', + }, +}; +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + portalConfiguration: { + enterpriseId: 'test-enterprise-id', + }, + dashboardInsights: mockedInsights, +}); + +const AIAnalyticsSummaryWrapper = props => ( + + + + , + + + +); + +describe('', () => { + it('should render action buttons correctly', () => { + const tree = renderer + .create(( + + )) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('should display AnalyticsDetailCard with learner_engagement data when Summarize Analytics button is clicked', () => { + const wrapper = mount(); + wrapper.find('[data-testid="summarize-analytics"]').first().simulate('click'); + + const tree = renderer + .create() + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('should display AnalyticsDetailCard with learner_progress data when Track Progress button is clicked', () => { + const wrapper = mount(); + wrapper.find('[data-testid="track-progress"]').first().simulate('click'); + + const tree = renderer + .create() + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('should handle null analytics data', () => { + const insightsData = { ...mockedInsights, learner_engagement: null }; + const wrapper = mount(); + wrapper.find('[data-testid="summarize-analytics"]').first().simulate('click'); + + const tree = renderer + .create() + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/Admin/AIAnalyticsSummarySkeleton.jsx b/src/components/Admin/AIAnalyticsSummarySkeleton.jsx new file mode 100644 index 0000000000..b44e6da4bd --- /dev/null +++ b/src/components/Admin/AIAnalyticsSummarySkeleton.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Skeleton, Stack } from '@edx/paragon'; + +const AIAnalyticsSummarySkeleton = () => ( + + + + +); + +export default AIAnalyticsSummarySkeleton; diff --git a/src/components/Admin/Admin.test.jsx b/src/components/Admin/Admin.test.jsx index 4362ae6805..1dad753b64 100644 --- a/src/components/Admin/Admin.test.jsx +++ b/src/components/Admin/Admin.test.jsx @@ -36,6 +36,10 @@ const store = mockStore({ number_of_users: 3, course_completions: 1, }, + dashboardInsights: { + loading: null, + insights: null, + }, }); const AdminWrapper = props => ( @@ -61,6 +65,8 @@ const AdminWrapper = props => ( url: '/', }} {...props} + fetchDashboardInsights={() => {}} + clearDashboardInsights={() => {}} /> @@ -77,6 +83,7 @@ describe('', () => { courseCompletions: 1, lastUpdatedDate: '2018-07-31T23:14:35Z', numberOfUsers: 3, + insights: null, }; describe('renders correctly', () => { @@ -290,6 +297,66 @@ describe('', () => { .toJSON(); expect(tree).toMatchSnapshot(); }); + + it('with no dashboard insights data', () => { + const insights = null; + const tree = renderer + .create(( + + )) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + describe('with dashboard insights data', () => { + it('renders dashboard insights data correctly', () => { + const insights = { + learner_progress: { + enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9', + enterprise_customer_name: 'Microsoft Corporation', + active_subscription_plan: true, + assigned_licenses: 0, + activated_licenses: 0, + assigned_licenses_percentage: 0.0, + activated_licenses_percentage: 0.0, + active_enrollments: 1026, + at_risk_enrollment_less_than_one_hour: 26, + at_risk_enrollment_end_date_soon: 15, + at_risk_enrollment_dormant: 918, + created_at: '2023-10-02T03:24:17Z', + }, + learner_engagement: { + enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9', + enterprise_customer_name: 'Microsoft Corporation', + enrolls: 49, + enrolls_prior: 45, + passed: 2, + passed_prior: 0, + engage: 67, + engage_prior: 50, + hours: 62, + hours_prior: 49, + contract_end_date: '2022-06-13T00:00:00Z', + active_contract: false, + created_at: '2023-10-02T03:24:40Z', + }, + }; + const tree = renderer + .create(( + + )) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + }); }); describe('handle changes to enterpriseId prop', () => { diff --git a/src/components/Admin/__snapshots__/AIAnalyticsSummary.test.jsx.snap b/src/components/Admin/__snapshots__/AIAnalyticsSummary.test.jsx.snap new file mode 100644 index 0000000000..2013f63e74 --- /dev/null +++ b/src/components/Admin/__snapshots__/AIAnalyticsSummary.test.jsx.snap @@ -0,0 +1,217 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should display AnalyticsDetailCard with learner_engagement data when Summarize Analytics button is clicked 1`] = ` +Array [ +
+ + +
, + ",", +] +`; + +exports[` should display AnalyticsDetailCard with learner_progress data when Track Progress button is clicked 1`] = ` +Array [ +
+ + +
, + ",", +] +`; + +exports[` should handle null analytics data 1`] = ` +Array [ +
+ + +
, + ",", +] +`; + +exports[` should render action buttons correctly 1`] = ` +Array [ +
+ + +
, + ",", +] +`; diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap index fd763719cf..5fdf48d830 100644 --- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap +++ b/src/components/Admin/__snapshots__/Admin.test.jsx.snap @@ -34,6 +34,13 @@ exports[` renders correctly calls fetchDashboardAnalytics prop 1`] = `
+
+
+
@@ -197,6 +204,13 @@ exports[` renders correctly with dashboard analytics data renders # cou
+
+
+
@@ -910,6 +924,13 @@ exports[` renders correctly with dashboard analytics data renders # of
+
+
+
@@ -1623,6 +1644,13 @@ exports[` renders correctly with dashboard analytics data renders # of
+
+
+
@@ -2340,6 +2368,13 @@ exports[` renders correctly with dashboard analytics data renders colla
+
+
+
@@ -3200,6 +3235,13 @@ exports[` renders correctly with dashboard analytics data renders full
+
+
+
@@ -4060,6 +4102,13 @@ exports[` renders correctly with dashboard analytics data renders inact
+
+
+
@@ -4777,6 +4826,13 @@ exports[` renders correctly with dashboard analytics data renders inact
+
+
+
@@ -5494,6 +5550,13 @@ exports[` renders correctly with dashboard analytics data renders learn
+
+
+
@@ -6211,6 +6274,13 @@ exports[` renders correctly with dashboard analytics data renders regis
+
+
+
@@ -6924,6 +6994,13 @@ exports[` renders correctly with dashboard analytics data renders top a
+
+
+
@@ -7607,7 +7684,7 @@ exports[` renders correctly with dashboard analytics data renders top a `; -exports[` renders correctly with error state 1`] = ` +exports[` renders correctly with dashboard insights data renders dashboard insights data correctly 1`] = `
renders correctly with error state 1`] = `
- - -
+
+ + + Track Progress +
- Loading... - - Loading - +
+

+ + 3 + + + + + + +

+

+ total number of learners registered +

+
+
+
+
+ +
+ +
-
-
-

- Full Report -

-
-
+ + 1 + + + + + + + +

+ learners enrolled in at least one course +

+
+
+ +
+
+
+
+
+
+

+ + 1 + + + + + + +

+

+ active learners in the past week +

+
+
+ +
+
+
+
+
+
+

+ + 1 + + + + + + +

+

+ course completions +

+
+
+ +
+
+
+
+
+
+ Loading... + + Loading + +
+
+
+
+
+
+
+

+ Full Report +

+
+
+
+
+
+
+
+
+
+
+
+
+ Showing data as of + July 31, 2018 +
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +`; + +exports[` renders correctly with error state 1`] = ` +
+
+
+

+ Learner Progress Report +

+
+
+ edX logo +
+
+
+
+
+

+ Overview +

+
+
+
+
+
+
+
+
+ + + + + +
+
+
+ Hey, nice to see you +
+

+ Try refreshing your screen + Network Error +

+
+
+
+
+
+
+
+
+ Loading... + + Loading + +
+
+
+
+
+
+
+

+ Full Report +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[` renders correctly with loading state 1`] = ` +
+
+
+

+ Learner Progress Report +

+
+
+ edX logo +
+
+
+
+
+

+ Overview +

+
+
+
+
+
+
+
+
+
+ Loading... +
+ + + ‌ + +
+
+ + + ‌ + +
+
+ + + ‌ + +
+
+ + + ‌ + +
+
+
+
+
+
+
+
+ Loading... + + Loading + +
+
+
+
+
+
+
+

+ Full Report +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[` renders correctly with no dashboard analytics data 1`] = ` +
+
+ Loading... +
+ + + ‌ + +
+
+ + + ‌ + +
+
+
+`; + +exports[` renders correctly with no dashboard insights data 1`] = ` +
+
+
+

+ Learner Progress Report +

+
+
+ edX logo +
+
+
+
+
+

+ Overview +

+
+
+
+
+
+
+
+
+
+
+

+ + 3 + + + + + + +

+

+ total number of learners registered +

+
+
+
+
+ +
+ +
+
+
+
+ className="card" + > +
+

+ + 1 + + + + + + +

+

+ learners enrolled in at least one course +

+
+
+ +
+
+
+
+
+
+

+ + 1 + + + + + + +

+

+ active learners in the past week +

+
+
+
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with loading state 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
- Loading... -
- - - ‌ - -
-
- - - ‌ - -
-
- + + 1 + + + + + + + +

+ course completions +

+
+
+
- - ‌ - -
- - - +
+
+ Details +
+
+ + + + + + Show details + + +
+
+ +
+ +
@@ -7908,6 +9617,216 @@ exports[` renders correctly with loading state 1`] = `
+
+
+ Showing data as of + July 31, 2018 +
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+
+
@@ -7916,48 +9835,3 @@ exports[` renders correctly with loading state 1`] = `
`; - -exports[` renders correctly with no dashboard analytics data 1`] = ` -
-
- Loading... -
- - - ‌ - -
-
- - - ‌ - -
-
-
-`; diff --git a/src/components/Admin/index.jsx b/src/components/Admin/index.jsx index 45db4b5059..b5ccee0bc3 100644 --- a/src/components/Admin/index.jsx +++ b/src/components/Admin/index.jsx @@ -24,12 +24,15 @@ import { formatTimestamp } from '../../utils'; import AdminCardsSkeleton from './AdminCardsSkeleton'; import { SubscriptionData } from '../subscriptions'; import EmbeddedSubscription from './EmbeddedSubscription'; +import AIAnalyticsSummary from './AIAnalyticsSummary'; +import AIAnalyticsSummarySkeleton from './AIAnalyticsSummarySkeleton'; class Admin extends React.Component { componentDidMount() { const { enterpriseId } = this.props; if (enterpriseId) { this.props.fetchDashboardAnalytics(enterpriseId); + this.props.fetchDashboardInsights(enterpriseId); } } @@ -37,12 +40,14 @@ class Admin extends React.Component { const { enterpriseId } = this.props; if (enterpriseId && enterpriseId !== prevProps.enterpriseId) { this.props.fetchDashboardAnalytics(enterpriseId); + this.props.fetchDashboardInsights(enterpriseId); } } componentWillUnmount() { // Clear the overview data this.props.clearDashboardAnalytics(); + this.props.clearDashboardInsights(); } getMetadataForAction(actionSlug) { @@ -281,6 +286,8 @@ class Admin extends React.Component { enterpriseId, match, location: { search }, + insights, + insightsLoading, } = this.props; const { params: { actionSlug } } = match; @@ -309,6 +316,13 @@ class Admin extends React.Component {

Overview

+
+
+ {insightsLoading ? : ( + insights && + )} +
+
{(error || loading) ? (
@@ -402,11 +416,15 @@ Admin.defaultProps = { }, csv: null, table: null, + insightsLoading: false, + insights: null, }; Admin.propTypes = { fetchDashboardAnalytics: PropTypes.func.isRequired, clearDashboardAnalytics: PropTypes.func.isRequired, + fetchDashboardInsights: PropTypes.func.isRequired, + clearDashboardInsights: PropTypes.func.isRequired, enterpriseId: PropTypes.string, searchEnrollmentsList: PropTypes.func.isRequired, location: PropTypes.shape({ @@ -431,6 +449,8 @@ Admin.propTypes = { }).isRequired, }).isRequired, table: PropTypes.shape({}), + insightsLoading: PropTypes.bool, + insights: PropTypes.objectOf(PropTypes.shape), }; export default Admin; diff --git a/src/containers/AdminPage/AdminPage.test.jsx b/src/containers/AdminPage/AdminPage.test.jsx index 29c0770f40..2e30fbade3 100644 --- a/src/containers/AdminPage/AdminPage.test.jsx +++ b/src/containers/AdminPage/AdminPage.test.jsx @@ -28,6 +28,10 @@ const store = mockStore({ table: { enrollments: {}, }, + dashboardInsights: { + loading: null, + insights: null, + }, }); describe('', () => { diff --git a/src/containers/AdminPage/index.jsx b/src/containers/AdminPage/index.jsx index 675cb1fbf5..502f7d18b6 100644 --- a/src/containers/AdminPage/index.jsx +++ b/src/containers/AdminPage/index.jsx @@ -9,6 +9,7 @@ import { import Admin from '../../components/Admin'; import { paginateTable } from '../../data/actions/table'; import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; +import { fetchDashboardInsights, clearDashboardInsights } from '../../data/actions/dashboardInsights'; const mapStateToProps = state => ({ loading: state.dashboardAnalytics.loading, @@ -21,6 +22,8 @@ const mapStateToProps = state => ({ enterpriseId: state.portalConfiguration.enterpriseId, csv: state.csv, table: state.table, + insightsLoading: state.dashboardInsights.loading, + insights: state.dashboardInsights.insights, }); const mapDispatchToProps = dispatch => ({ @@ -33,6 +36,12 @@ const mapDispatchToProps = dispatch => ({ searchEnrollmentsList: () => { dispatch(paginateTable('enrollments', EnterpriseDataApiService.fetchCourseEnrollments)); }, + fetchDashboardInsights: (enterpriseId) => { + dispatch(fetchDashboardInsights(enterpriseId)); + }, + clearDashboardInsights: () => { + dispatch(clearDashboardInsights()); + }, }); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Admin)); diff --git a/src/containers/EnterpriseApp/EnterpriseApp.test.jsx b/src/containers/EnterpriseApp/EnterpriseApp.test.jsx index bde4287dcc..6281ab463d 100644 --- a/src/containers/EnterpriseApp/EnterpriseApp.test.jsx +++ b/src/containers/EnterpriseApp/EnterpriseApp.test.jsx @@ -91,6 +91,7 @@ const initialState = { isExpanded: false, isExpandedByToggle: false, }, + dashboardInsights: {}, }; // eslint-disable-next-line react/prop-types diff --git a/src/data/actions/dashboardInsights.js b/src/data/actions/dashboardInsights.js new file mode 100644 index 0000000000..b9b65a040d --- /dev/null +++ b/src/data/actions/dashboardInsights.js @@ -0,0 +1,41 @@ +import { logError } from '@edx/frontend-platform/logging'; +import { + FETCH_DASHBOARD_INSIGHTS_REQUEST, + FETCH_DASHBOARD_INSIGHTS_SUCCESS, + FETCH_DASHBOARD_INSIGHTS_FAILURE, + CLEAR_DASHBOARD_INSIGHTS, +} from '../constants/dashboardInsights'; +import EnterpriseDataApiService from '../services/EnterpriseDataApiService'; + +const fetchDashboardInsightsRequest = () => ({ type: FETCH_DASHBOARD_INSIGHTS_REQUEST }); +const fetchDashboardInsightsSuccess = data => ({ + type: FETCH_DASHBOARD_INSIGHTS_SUCCESS, + payload: { data }, +}); +const fetchDashboardInsightsFailure = error => ({ + type: FETCH_DASHBOARD_INSIGHTS_FAILURE, + payload: { error }, +}); + +const fetchDashboardInsights = enterpriseId => ( + (dispatch) => { + dispatch(fetchDashboardInsightsRequest()); + return EnterpriseDataApiService.fetchDashboardInsights(enterpriseId) + .then((response) => { + dispatch(fetchDashboardInsightsSuccess(response.data)); + }) + .catch((error) => { + logError(error); + dispatch(fetchDashboardInsightsFailure(error)); + }); + } +); + +const clearDashboardInsights = () => dispatch => (dispatch({ + type: CLEAR_DASHBOARD_INSIGHTS, +})); + +export { + fetchDashboardInsights, + clearDashboardInsights, +}; diff --git a/src/data/constants/dashboardInsights.js b/src/data/constants/dashboardInsights.js new file mode 100644 index 0000000000..05b81a28e9 --- /dev/null +++ b/src/data/constants/dashboardInsights.js @@ -0,0 +1,11 @@ +const FETCH_DASHBOARD_INSIGHTS_REQUEST = 'FETCH_DASHBOARD_INSIGHTS_REQUEST'; +const FETCH_DASHBOARD_INSIGHTS_SUCCESS = 'FETCH_DASHBOARD_INSIGHTS_SUCCESS'; +const FETCH_DASHBOARD_INSIGHTS_FAILURE = 'FETCH_DASHBOARD_INSIGHTS_FAILURE'; +const CLEAR_DASHBOARD_INSIGHTS = 'CLEAR_DASHBOARD_INSIGHTS'; + +export { + FETCH_DASHBOARD_INSIGHTS_REQUEST, + FETCH_DASHBOARD_INSIGHTS_SUCCESS, + FETCH_DASHBOARD_INSIGHTS_FAILURE, + CLEAR_DASHBOARD_INSIGHTS, +}; diff --git a/src/data/reducers/dashboardInsights.js b/src/data/reducers/dashboardInsights.js new file mode 100644 index 0000000000..6c25972617 --- /dev/null +++ b/src/data/reducers/dashboardInsights.js @@ -0,0 +1,47 @@ +import { + FETCH_DASHBOARD_INSIGHTS_REQUEST, + FETCH_DASHBOARD_INSIGHTS_SUCCESS, + FETCH_DASHBOARD_INSIGHTS_FAILURE, + CLEAR_DASHBOARD_INSIGHTS, +} from '../constants/dashboardInsights'; + +const initialState = { + loading: false, + error: null, + insights: null, +}; + +const dashboardInsights = (state = initialState, action) => { + switch (action.type) { + case FETCH_DASHBOARD_INSIGHTS_REQUEST: + return { + ...state, + loading: true, + error: null, + }; + case FETCH_DASHBOARD_INSIGHTS_SUCCESS: + return { + ...state, + loading: false, + insights: action.payload.data, + }; + case FETCH_DASHBOARD_INSIGHTS_FAILURE: + return { + ...state, + loading: false, + error: action.payload.error, + insights: null, + }; + case CLEAR_DASHBOARD_INSIGHTS: + return { + ...state, + loading: false, + error: null, + insights: null, + }; + default: + return state; + } +}; + +export default dashboardInsights; diff --git a/src/data/reducers/dashboardInsights.test.js b/src/data/reducers/dashboardInsights.test.js new file mode 100644 index 0000000000..7e2d3cd713 --- /dev/null +++ b/src/data/reducers/dashboardInsights.test.js @@ -0,0 +1,107 @@ +import dashboardInsights from './dashboardInsights'; +import { + FETCH_DASHBOARD_INSIGHTS_REQUEST, + FETCH_DASHBOARD_INSIGHTS_SUCCESS, + FETCH_DASHBOARD_INSIGHTS_FAILURE, + CLEAR_DASHBOARD_INSIGHTS, +} from '../constants/dashboardInsights'; + +const initialState = { + loading: false, + error: null, + insights: null, +}; + +const mockInsightsData = { + learner_progress: { + enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9', + enterprise_customer_name: 'Microsoft Corporation', + active_subscription_plan: true, + assigned_licenses: 0, + activated_licenses: 0, + assigned_licenses_percentage: 0.0, + activated_licenses_percentage: 0.0, + active_enrollments: 1026, + at_risk_enrollment_less_than_one_hour: 26, + at_risk_enrollment_end_date_soon: 15, + at_risk_enrollment_dormant: 918, + created_at: '2023-10-02T03:24:17Z', + }, + learner_engagement: { + enterprise_customer_uuid: 'aac56d39-f38d-4510-8ef9-085cab048ea9', + enterprise_customer_name: 'Microsoft Corporation', + enrolls: 49, + enrolls_prior: 45, + passed: 2, + passed_prior: 0, + engage: 67, + engage_prior: 50, + hours: 62, + hours_prior: 49, + contract_end_date: '2022-06-13T00:00:00Z', + active_contract: false, + created_at: '2023-10-02T03:24:40Z', + }, +}; + +describe('dashboardInsights reducer', () => { + it('has initial state', () => { + expect(dashboardInsights(undefined, {})).toEqual(initialState); + }); + + it('updates fetch insights request state', () => { + const expected = { + ...initialState, + loading: true, + error: null, + }; + expect(dashboardInsights(undefined, { + type: FETCH_DASHBOARD_INSIGHTS_REQUEST, + })).toEqual(expected); + }); + + it('updates fetch insights success state', () => { + const expected = { + ...initialState, + loading: false, + insights: mockInsightsData, + error: null, + }; + expect(dashboardInsights(undefined, { + type: FETCH_DASHBOARD_INSIGHTS_SUCCESS, + payload: { data: mockInsightsData }, + })).toEqual(expected); + }); + + it('updates fetch insights failure state', () => { + const error = Error('Network Request'); + const expected = { + ...initialState, + loading: false, + error, + insights: null, + }; + expect(dashboardInsights(undefined, { + type: FETCH_DASHBOARD_INSIGHTS_FAILURE, + payload: { error }, + })).toEqual(expected); + }); + + it('updates clear insights state', () => { + const state = dashboardInsights(undefined, { + type: FETCH_DASHBOARD_INSIGHTS_SUCCESS, + payload: { data: mockInsightsData }, + }); + + const expected = { + ...initialState, + loading: false, + error: null, + insights: null, + }; + + expect(dashboardInsights(state, { + type: CLEAR_DASHBOARD_INSIGHTS, + })).toEqual(expected); + }); +}); diff --git a/src/data/reducers/index.js b/src/data/reducers/index.js index 27b18f99f5..c3d5abfaf3 100644 --- a/src/data/reducers/index.js +++ b/src/data/reducers/index.js @@ -13,6 +13,7 @@ import licenseRevoke from './licenseRevoke'; import emailTemplate from './emailTemplate'; import licenseRemind from './licenseRemind'; import userSubscription from './userSubscription'; +import dashboardInsights from './dashboardInsights'; const identityReducer = (state) => { const newState = { ...state }; @@ -36,6 +37,7 @@ const rootReducer = combineReducers({ emailTemplate, licenseRemind, userSubscription, + dashboardInsights, }); export default rootReducer; diff --git a/src/data/services/EnterpriseDataApiService.js b/src/data/services/EnterpriseDataApiService.js index bf00da3de6..ef7b5078e5 100644 --- a/src/data/services/EnterpriseDataApiService.js +++ b/src/data/services/EnterpriseDataApiService.js @@ -9,6 +9,8 @@ class EnterpriseDataApiService { static enterpriseBaseUrl = `${configuration.DATA_API_BASE_URL}/enterprise/api/v1/enterprise/`; + static enterpriseAdminBaseUrl = `${configuration.DATA_API_BASE_URL}/enterprise/api/v1/admin/`; + static fetchDashboardAnalytics(enterpriseId) { const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/enrollments/overview/`; return EnterpriseDataApiService.apiClient().get(url); @@ -111,6 +113,11 @@ class EnterpriseDataApiService { const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/${endpoint}/?${queryParams.toString()}`; return EnterpriseDataApiService.apiClient().get(url); } + + static fetchDashboardInsights(enterpriseId) { + const url = `${EnterpriseDataApiService.enterpriseAdminBaseUrl}insights/${enterpriseId}`; + return EnterpriseDataApiService.apiClient().get(url); + } } export default EnterpriseDataApiService; diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js index 2ed35e8274..31bc12fce7 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -379,6 +379,11 @@ class LmsApiService { }; return LmsApiService.apiClient().put(`${LmsApiService.apiCredentialsUrl}${enterpriseUUID}/regenerate_credentials`, requestData); } + + static generateAIAnalyticsSummary(enterpriseUUID, formData) { + const url = `${LmsApiService.baseUrl}/enterprise/api/v1/analytics-summary/${enterpriseUUID}/`; + return LmsApiService.apiClient().post(url, formData); + } } export default LmsApiService; From 279d0e30759f5aa45a236e84669809051f9906ac Mon Sep 17 00:00:00 2001 From: mahamakifdar19 Date: Wed, 11 Oct 2023 12:26:38 +0500 Subject: [PATCH 036/124] fix: removed trailing / from ai analytics enpoint url --- src/data/services/LmsApiService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js index 31bc12fce7..18f2cca502 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -381,7 +381,7 @@ class LmsApiService { } static generateAIAnalyticsSummary(enterpriseUUID, formData) { - const url = `${LmsApiService.baseUrl}/enterprise/api/v1/analytics-summary/${enterpriseUUID}/`; + const url = `${LmsApiService.baseUrl}/enterprise/api/v1/analytics-summary/${enterpriseUUID}`; return LmsApiService.apiClient().post(url, formData); } } From f902ee1ee26efb4a39dd13e73ed27d470fb4f9ea Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:28:14 -0600 Subject: [PATCH 037/124] feat: Adding search/filtering for top-down assignment (#1047) * fix: draft PR * Merge branch 'master' into kiram15/ENT-7339 * fix: adding search filters * fix: removing categories from page * fix: extra fixes * fix: adding tests and fixes * fix: PR review * fix: PR reviews --- __mocks__/react-instantsearch-dom.jsx | 4 +- .../Admin/EmbeddedSubscription.test.jsx | 1 - .../Admin/SubscriptionDetailPage.test.jsx | 1 - .../licenses/LicenseAllocationHeader.test.jsx | 2 - .../CourseSearchResults.test.jsx | 1 - .../stepper/BulkEnrollmentSubmit.test.jsx | 1 - .../CodeAssignmentModal.test.jsx | 2 +- .../tests/ContentConfirmContentCard.test.jsx | 2 +- .../HighlightStepperConfirmContent.test.jsx | 1 - ...ghlightStepperSelectContentSearch.test.jsx | 1 - .../ContentHighlights/data/constants.js | 4 +- .../data/tests/constants.test.js | 2 +- .../EnterpriseApp/EnterpriseApp.test.jsx | 2 +- .../EnterpriseList/EnterpriseList.test.jsx | 1 - .../BudgetDetailCatalogTabContents.jsx | 52 +++++- .../cards/CourseCard.jsx | 97 +++++++++++ .../cards/CourseCard.test.jsx | 82 ++++++++++ .../data/constants.js | 10 ++ .../data/hooks/hooks.js | 13 ++ .../learner-credit-management/data/utils.js | 6 + .../learner-credit-management/index.js | 4 + .../learner-credit.scss | 25 +++ .../search/CatalogSearch.jsx | 44 +++++ .../search/CatalogSearchResults.jsx | 149 +++++++++++++++++ .../tests/CatalogSearch.test.jsx | 42 +++++ .../tests/CatalogSearchResults.test.jsx | 154 ++++++++++++++++++ .../settings/tests/SettingsTabs.test.jsx | 1 - .../bulk-actions/EnrollBulkAction.test.jsx | 1 - .../bulk-actions/RemindBulkAction.test.jsx | 1 - .../bulk-actions/RevokeBulkAction.test.jsx | 1 - .../tests/index.test.jsx | 1 - .../tests/MultipleSubscriptionsPage.test.jsx | 1 - .../SubscriptionExpirationBanner.test.jsx | 2 +- .../SubscriptionExpirationModals.test.jsx | 2 +- src/components/test/testUtils.jsx | 1 - .../EnterpriseApp/EnterpriseApp.test.jsx | 5 +- src/data/hooks.js | 15 +- 37 files changed, 696 insertions(+), 38 deletions(-) create mode 100644 src/components/learner-credit-management/cards/CourseCard.jsx create mode 100644 src/components/learner-credit-management/cards/CourseCard.test.jsx create mode 100644 src/components/learner-credit-management/data/hooks/hooks.js create mode 100644 src/components/learner-credit-management/index.js create mode 100644 src/components/learner-credit-management/learner-credit.scss create mode 100644 src/components/learner-credit-management/search/CatalogSearch.jsx create mode 100644 src/components/learner-credit-management/search/CatalogSearchResults.jsx create mode 100644 src/components/learner-credit-management/tests/CatalogSearch.test.jsx create mode 100644 src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx diff --git a/__mocks__/react-instantsearch-dom.jsx b/__mocks__/react-instantsearch-dom.jsx index f62f6554ca..4444b52c33 100644 --- a/__mocks__/react-instantsearch-dom.jsx +++ b/__mocks__/react-instantsearch-dom.jsx @@ -15,8 +15,8 @@ const advertised_course_run = { /* eslint-disable camelcase */ const fakeHits = [ - { objectID: '1', aggregation_key: 'course:Bees101', title: 'bla', advertised_course_run, key: 'Bees101' }, - { objectID: '2', aggregation_key: 'course:Wasps200', title: 'blp', advertised_course_run, key: 'Wasps200' }, + { objectID: '1', aggregation_key: 'course:Bees101', title: 'bla', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Bees101' }, + { objectID: '2', aggregation_key: 'course:Wasps200', title: 'blp', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Wasps200' }, ]; /* eslint-enable camelcase */ diff --git a/src/components/Admin/EmbeddedSubscription.test.jsx b/src/components/Admin/EmbeddedSubscription.test.jsx index 16e36f5f97..5c767330d3 100644 --- a/src/components/Admin/EmbeddedSubscription.test.jsx +++ b/src/components/Admin/EmbeddedSubscription.test.jsx @@ -36,7 +36,6 @@ const defaultAppContext = { }, }; -// eslint-disable-next-line react/prop-types const AppContextProvider = ({ children }) => ( {children} diff --git a/src/components/Admin/SubscriptionDetailPage.test.jsx b/src/components/Admin/SubscriptionDetailPage.test.jsx index 82d6b48369..7287107f1e 100644 --- a/src/components/Admin/SubscriptionDetailPage.test.jsx +++ b/src/components/Admin/SubscriptionDetailPage.test.jsx @@ -46,7 +46,6 @@ const initialSubsidyRequestContextValue = { }, }; -// eslint-disable-next-line react/prop-types const AppContextProvider = ({ children }) => ( {children} diff --git a/src/components/Admin/licenses/LicenseAllocationHeader.test.jsx b/src/components/Admin/licenses/LicenseAllocationHeader.test.jsx index bba9f04e4f..8179fad6d0 100644 --- a/src/components/Admin/licenses/LicenseAllocationHeader.test.jsx +++ b/src/components/Admin/licenses/LicenseAllocationHeader.test.jsx @@ -11,7 +11,6 @@ import { SubsidyRequestsContext } from '../../subsidy-requests'; describe('LicenseAllocationHeader', () => { const mockStore = configureMockStore(); - // eslint-disable-next-line react/prop-types const SubscriptionDetailContextWrapper = ({ children }) => ( // eslint-disable-next-line react/jsx-no-constructed-context-values { ); - // eslint-disable-next-line react/prop-types const SubsidyRequestsContextWrapper = ({ children }) => ( // eslint-disable-next-line react/jsx-no-constructed-context-values ( diff --git a/src/components/BulkEnrollmentPage/stepper/BulkEnrollmentSubmit.test.jsx b/src/components/BulkEnrollmentPage/stepper/BulkEnrollmentSubmit.test.jsx index 58a42934cf..b72e8e3dee 100644 --- a/src/components/BulkEnrollmentPage/stepper/BulkEnrollmentSubmit.test.jsx +++ b/src/components/BulkEnrollmentPage/stepper/BulkEnrollmentSubmit.test.jsx @@ -85,7 +85,6 @@ const bulkEnrollWithCoursesSelectedRows = { courses: [selectedCourses, coursesDispatch], }; -// eslint-disable-next-line react/prop-types const BulkEnrollmentSubmitWrapper = ({ bulkEnrollInfo = defaultBulkEnrollInfo, ...props }) => ( diff --git a/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx b/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx index a62e71dc20..b06b247812 100644 --- a/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx +++ b/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx @@ -20,7 +20,7 @@ import { jest.mock('redux-form', () => ({ ...jest.requireActual('redux-form'), - // eslint-disable-next-line react/prop-types + Field: ({ label, ...rest }) =>
{label}
, })); diff --git a/src/components/ContentHighlights/HighlightStepper/tests/ContentConfirmContentCard.test.jsx b/src/components/ContentHighlights/HighlightStepper/tests/ContentConfirmContentCard.test.jsx index 1f8f8f3a7a..b5b836dac8 100644 --- a/src/components/ContentHighlights/HighlightStepper/tests/ContentConfirmContentCard.test.jsx +++ b/src/components/ContentHighlights/HighlightStepper/tests/ContentConfirmContentCard.test.jsx @@ -45,7 +45,7 @@ const searchClient = algoliasearch( ); const ContentHighlightContentCardWrapper = ({ - // eslint-disable-next-line react/prop-types + store = mockStore(initialState), }) => { const contextValue = useState({ diff --git a/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx b/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx index 68022a1df8..34a9c64a67 100644 --- a/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx +++ b/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperConfirmContent.test.jsx @@ -30,7 +30,6 @@ const searchClient = algoliasearch( configuration.ALGOLIA.SEARCH_API_KEY, ); -// eslint-disable-next-line react/prop-types const HighlightStepperConfirmContentWrapper = ({ children, currentSelectedRowIds = [] }) => { const contextValue = useState({ stepperModal: { diff --git a/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperSelectContentSearch.test.jsx b/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperSelectContentSearch.test.jsx index c4f329b011..4d489c6d03 100644 --- a/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperSelectContentSearch.test.jsx +++ b/src/components/ContentHighlights/HighlightStepper/tests/HighlightStepperSelectContentSearch.test.jsx @@ -38,7 +38,6 @@ const searchClient = algoliasearch( configuration.ALGOLIA.SEARCH_API_KEY, ); -// eslint-disable-next-line react/prop-types const HighlightStepperSelectContentSearchWrapper = ({ children, currentSelectedRowIds = [] }) => { const contextValue = useState({ stepperModal: { diff --git a/src/components/ContentHighlights/data/constants.js b/src/components/ContentHighlights/data/constants.js index 93269fab0a..fafc699ba4 100644 --- a/src/components/ContentHighlights/data/constants.js +++ b/src/components/ContentHighlights/data/constants.js @@ -14,7 +14,7 @@ export const sanitizeAndParseHTML = (htmlString) => { // Set to false before pushing PR!! otherwise set to true to enable local testing of ContentHighlights components // Test will fail as additional check to ensure this is set to false before pushing PR export const TEST_FLAG = false; -// Test entepriseId for Content Highlights to display card selections and confirmation +// Test enterpriseId for Content Highlights to display card selections and confirmation export const testEnterpriseId = '943b1234-58cf-4376-b8e0-0efcbf4bfdf9'; // function that passes through enterpriseId if TEST_FLAG is false, otherwise returns local testing enterpriseId export const ENABLE_TESTING = (enterpriseId, enableTest = TEST_FLAG) => { @@ -42,7 +42,7 @@ export const TAB_TITLES = { // Max length of highlight title in stepper export const MAX_HIGHLIGHT_TITLE_LENGTH = 60; -// Max highlight sets per enteprise curation +// Max highlight sets per enterprise curation export const MAX_HIGHLIGHT_SETS_PER_ENTERPRISE_CURATION = 12; // Max number of content items per highlight set diff --git a/src/components/ContentHighlights/data/tests/constants.test.js b/src/components/ContentHighlights/data/tests/constants.test.js index b12f3f1982..00125b4f57 100644 --- a/src/components/ContentHighlights/data/tests/constants.test.js +++ b/src/components/ContentHighlights/data/tests/constants.test.js @@ -33,7 +33,7 @@ describe('constants', () => { }); it('renders title name in string functions', () => { const highlightTitle = 'test-title'; - // eslint-disable-next-line react/prop-types + const TestComponent = ({ children }) => (

{children} diff --git a/src/components/EnterpriseApp/EnterpriseApp.test.jsx b/src/components/EnterpriseApp/EnterpriseApp.test.jsx index 2aeace95d8..04ea668d69 100644 --- a/src/components/EnterpriseApp/EnterpriseApp.test.jsx +++ b/src/components/EnterpriseApp/EnterpriseApp.test.jsx @@ -36,7 +36,7 @@ const EnterpriseAppContextProvider = ({ children }) => ( jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), __esModule: true, - // eslint-disable-next-line react/prop-types + Route: (props) => {props.path}, Switch: (props) => props.children, Redirect: () => 'Redirect', diff --git a/src/components/EnterpriseList/EnterpriseList.test.jsx b/src/components/EnterpriseList/EnterpriseList.test.jsx index 47b04098c0..d466f08366 100644 --- a/src/components/EnterpriseList/EnterpriseList.test.jsx +++ b/src/components/EnterpriseList/EnterpriseList.test.jsx @@ -35,7 +35,6 @@ const store = mockStore({ }, }); -// eslint-disable-next-line react/prop-types const EnterpriseListWrapper = ({ initialEntries, ...rest }) => ( diff --git a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx index 83745cdd7b..a99f268db3 100644 --- a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx @@ -1,13 +1,49 @@ import React from 'react'; +import { InstantSearch } from 'react-instantsearch-dom'; +import algoliasearch from 'algoliasearch/lite'; import { Row, Col } from '@edx/paragon'; -const BudgetDetailCatalogTabContents = () => ( - - -

Budget Name

-

TODO

- - -); +import { SearchData, SEARCH_FACET_FILTERS } from '@edx/frontend-enterprise-catalog-search'; +import CatalogSearch from './search/CatalogSearch'; +import { LANGUAGE_REFINEMENT, LEARNING_TYPE_REFINEMENT } from './data'; +import { configuration } from '../../config'; + +const BudgetDetailCatalogTabContents = () => { + const language = { + attribute: LANGUAGE_REFINEMENT, + title: 'Language', + }; + const learningType = { + attribute: LEARNING_TYPE_REFINEMENT, + title: 'Learning Type', + }; + // Add search facet filters if they don't exist in the list yet + [language, learningType].forEach((refinement) => { + if (!SEARCH_FACET_FILTERS.some((filter) => filter.attribute === refinement.attribute)) { + SEARCH_FACET_FILTERS.push(refinement); + } + }); + + const searchClient = algoliasearch( + configuration.ALGOLIA.APP_ID, + configuration.ALGOLIA.SEARCH_API_KEY, + ); + return ( + + + + + + + + + + ); +}; export default BudgetDetailCatalogTabContents; diff --git a/src/components/learner-credit-management/cards/CourseCard.jsx b/src/components/learner-credit-management/cards/CourseCard.jsx new file mode 100644 index 0000000000..9284369239 --- /dev/null +++ b/src/components/learner-credit-management/cards/CourseCard.jsx @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +// variables taken from algolia not in camelcase +import React from 'react'; +import PropTypes from 'prop-types'; + +import { camelCaseObject } from '@edx/frontend-platform'; +import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; +import { + Badge, Button, Card, Hyperlink, +} from '@edx/paragon'; +import { EXEC_COURSE_TYPE } from '../data/constants'; +import { formatDate } from '../data/utils'; + +const CourseCard = ({ + onClick, original, +}) => { + const { + title, + cardImageUrl, + courseType, + normalizedMetadata, + partners, + } = camelCaseObject(original); + + let priceText; + const altText = `${title} course image`; + + return ( + onClick(original)} + orientation="horizontal" + tabIndex="0" + > + +
+
+

{title}

+

{partners[0]?.name}

+ {courseType === EXEC_COURSE_TYPE && ( + + Executive Education + + )} + {courseType !== EXEC_COURSE_TYPE && ( +

+ )} +

+ Starts {formatDate(normalizedMetadata?.start_date)} • + Learner must register by {formatDate(normalizedMetadata?.enroll_by_date)} +

+
+ +

{priceText}

+

Per learner price

+ + + + + +
+
+
+ ); +}; + +CourseCard.defaultProps = { + onClick: () => {}, +}; + +CourseCard.propTypes = { + onClick: PropTypes.func, + original: PropTypes.shape({ + title: PropTypes.string, + cardImageUrl: PropTypes.string, + partners: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + logo_image_url: PropTypes.string, + }), + ), + normalizedMetadata: PropTypes.shape({ + startDate: PropTypes.string, + endDate: PropTypes.string, + enrollByDate: PropTypes.string, + }), + courseType: PropTypes.string, + }).isRequired, +}; + +export default CourseCard; diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/CourseCard.test.jsx new file mode 100644 index 0000000000..963137e178 --- /dev/null +++ b/src/components/learner-credit-management/cards/CourseCard.test.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import CourseCard from './CourseCard'; +import { CONTENT_TYPE_COURSE, EXEC_ED_TITLE } from '../data/constants'; + +jest.mock('@edx/frontend-platform', () => ({ + ...jest.requireActual('@edx/frontend-platform'), +})); + +const TEST_CATALOG = ['ayylmao']; + +const originalData = { + title: 'Course Title', + card_image_url: undefined, + partners: [{ logo_image_url: '', name: 'Course Provider' }], + first_enrollable_paid_seat_price: 100, + original_image_url: '', + enterprise_catalog_query_titles: TEST_CATALOG, + advertised_course_run: { pacing_type: 'self_paced' }, +}; + +const defaultProps = { + original: originalData, + learningType: CONTENT_TYPE_COURSE, +}; + +const execEdData = { + title: 'Exec Ed Course Title', + card_image_url: undefined, + partners: [{ logo_image_url: '', name: 'Course Provider' }], + first_enrollable_paid_seat_price: 100, + original_image_url: '', + enterprise_catalog_query_titles: TEST_CATALOG, + advertised_course_run: { pacing_type: 'instructor_paced' }, + entitlements: [{ price: '999.00' }], +}; + +const execEdProps = { + original: execEdData, + learningType: EXEC_ED_TITLE, +}; + +describe('Course card works as expected', () => { + test('card renders as expected', () => { + render( + + + , + ); + expect(screen.queryByText(defaultProps.original.title)).toBeInTheDocument(); + expect( + screen.queryByText(defaultProps.original.partners[0].name), + ).toBeInTheDocument(); + expect(screen.queryByText('Course Title')).toBeInTheDocument(); + expect(screen.queryByText('Per learner price')).toBeInTheDocument(); + }); + test('exec ed card renders as expected', () => { + render( + + + , + ); + expect(screen.queryByText(execEdProps.original.title)).toBeInTheDocument(); + expect( + screen.queryByText(execEdProps.original.partners[0].name), + ).toBeInTheDocument(); + expect(screen.queryByText('Exec Ed Course Title')).toBeInTheDocument(); + }); + test('test card renders default image', async () => { + render( + + + , + ); + const imageAltText = `${originalData.title} course image`; + fireEvent.error(screen.getByAltText(imageAltText)); + await expect(screen.getByAltText(imageAltText).src).not.toBeUndefined; + }); +}); diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index defca8d674..37f453bd4a 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -24,3 +24,13 @@ export const BUDGET_DETAIL_TAB_LABELS = { [BUDGET_DETAIL_ACTIVITY_TAB]: 'Activity', [BUDGET_DETAIL_CATALOG_TAB]: 'Catalog', }; + +// Facet filters +export const LEARNING_TYPE_REFINEMENT = 'learning_type'; +export const LANGUAGE_REFINEMENT = 'language'; + +// Learning types +export const CONTENT_TYPE_COURSE = 'course'; +export const EXEC_ED_TITLE = 'Executive Education'; + +export const EXEC_COURSE_TYPE = 'executive-education-2u'; diff --git a/src/components/learner-credit-management/data/hooks/hooks.js b/src/components/learner-credit-management/data/hooks/hooks.js new file mode 100644 index 0000000000..306ad58fde --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/hooks.js @@ -0,0 +1,13 @@ +import { useMemo, useState } from 'react'; + +import { CONTENT_TYPE_COURSE } from '../constants'; + +// eslint-disable-next-line import/prefer-default-export +export const useSelectedCourse = () => { + const [course, setCourse] = useState(null); + const isCourse = useMemo( + () => course?.contentType === CONTENT_TYPE_COURSE, + [course], + ); + return [course, setCourse, isCourse]; +}; diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 376479f6a5..4705d62507 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -1,4 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; +import dayjs from 'dayjs'; + import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, @@ -181,3 +183,7 @@ export const orderOffers = (offers) => { return offers; }; + +export function formatDate(date) { + return dayjs(date).format('MMM D, YYYY'); +} diff --git a/src/components/learner-credit-management/index.js b/src/components/learner-credit-management/index.js new file mode 100644 index 0000000000..ff16dee97e --- /dev/null +++ b/src/components/learner-credit-management/index.js @@ -0,0 +1,4 @@ +import MultipleBudgetsPage from './MultipleBudgetsPage'; +import './learner-credit.scss'; + +export default MultipleBudgetsPage; diff --git a/src/components/learner-credit-management/learner-credit.scss b/src/components/learner-credit-management/learner-credit.scss new file mode 100644 index 0000000000..c26c3e9859 --- /dev/null +++ b/src/components/learner-credit-management/learner-credit.scss @@ -0,0 +1,25 @@ +.card-container { + display: flex; + padding: 1rem; + flex-grow: 1; + justify-content: space-between; + + .section-1 { + flex-direction: column; + } + .section-2 { + margin-left: 0; + text-align: end !important; + min-width: 400px; + padding-right: 0; + justify-content: space-between; + .footer { + justify-content: end; + padding: 0; + } + } +} + +.badge { + margin: 4px; +} diff --git a/src/components/learner-credit-management/search/CatalogSearch.jsx b/src/components/learner-credit-management/search/CatalogSearch.jsx new file mode 100644 index 0000000000..43cc7aae8d --- /dev/null +++ b/src/components/learner-credit-management/search/CatalogSearch.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import algoliasearch from 'algoliasearch/lite'; +import { Configure, InstantSearch } from 'react-instantsearch-dom'; + +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { SearchHeader } from '@edx/frontend-enterprise-catalog-search'; + +import { configuration } from '../../../config'; +import CatalogSearchResults from './CatalogSearchResults'; + +const CatalogSearch = () => { + const { budgetId } = useParams(); + const searchClient = algoliasearch(configuration.ALGOLIA.APP_ID, configuration.ALGOLIA.SEARCH_API_KEY); + + const searchFilters = `enterprise_catalog_query_uuids:${budgetId}`; + + return ( +
+ + +
+ + +
+ +
+
+ ); +}; + +export default CatalogSearch; diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx new file mode 100644 index 0000000000..60c20e0262 --- /dev/null +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -0,0 +1,149 @@ +import React, { useEffect, useMemo } from 'react'; +import { connectStateResults } from 'react-instantsearch-dom'; +import PropTypes from 'prop-types'; + +import { SearchPagination } from '@edx/frontend-enterprise-catalog-search'; +import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n'; +import { + Alert, CardView, DataTable, Skeleton, +} from '@edx/paragon'; + +import CourseCard from '../cards/CourseCard'; + +export const ERROR_MESSAGE = 'An error occurred while retrieving data'; + +export const SKELETON_DATA_TESTID = 'enterprise-catalog-skeleton'; + +/** + * The core search results rendering component. + * + * Wrapping this in `connectStateResults()` will inject the first few props. + * + * @param {object} args arguments + * @param {object} args.searchResults Results of search (see: `connectStateResults``) + * @param {Boolean} args.isSearchStalled Whether search is stalled (see: `connectStateResults`) + * @param {object} args.error Error with `message` field if available (see: `connectStateResults``) + */ + +export const BaseCatalogSearchResults = ({ + searchResults, + // algolia recommends this prop instead of searching + isSearchStalled, + error, + setNoContent, +}) => { + const courseColumns = useMemo( + () => [ + { + Header: 'Course name', + accessor: 'title', + }, + { + Header: 'Partner', + accessor: 'partners[0].name', + }, + { + Header: 'A la carte course price', + accessor: 'first_enrollable_paid_seat_price', + }, + { + Header: 'Associated catalogs', + accessor: 'enterprise_catalog_query_titles', + }, + ], + [], + ); + + const tableData = useMemo( + () => searchResults?.hits || [], + [searchResults?.hits], + ); + + const renderCardComponent = (props) => ; + + useEffect(() => { + setNoContent(searchResults === null || searchResults?.nbHits === 0); + }, [searchResults, setNoContent]); + + if (isSearchStalled) { + return ( +
+ +
+ ); + } + if (error) { + return ( + + + + ); + } + + return ( +
+ + + renderCardComponent(props)} + /> + + + +
+ ); +}; + +BaseCatalogSearchResults.defaultProps = { + searchResults: { disjunctiveFacetsRefinements: [], nbHits: 0, hits: [] }, + error: null, + paginationComponent: SearchPagination, + row: null, + preview: false, + setNoContent: () => {}, +}; + +BaseCatalogSearchResults.propTypes = { + // from Algolia + searchResults: PropTypes.shape({ + _state: PropTypes.shape({ + disjunctiveFacetsRefinements: PropTypes.shape({}), + }), + disjunctiveFacetsRefinements: PropTypes.arrayOf(PropTypes.shape({})), + nbHits: PropTypes.number, + hits: PropTypes.arrayOf(PropTypes.shape({})), + nbPages: PropTypes.number, + hitsPerPage: PropTypes.number, + page: PropTypes.number, + }), + isSearchStalled: PropTypes.bool.isRequired, + error: PropTypes.shape({ + message: PropTypes.string, + }), + + searchState: PropTypes.shape({ + page: PropTypes.number, + }).isRequired, + paginationComponent: PropTypes.func, + // eslint-disable-next-line react/no-unused-prop-types + row: PropTypes.string, + contentType: PropTypes.string.isRequired, + preview: PropTypes.bool, + setNoContent: PropTypes.func, +}; + +export default connectStateResults(injectIntl(BaseCatalogSearchResults)); diff --git a/src/components/learner-credit-management/tests/CatalogSearch.test.jsx b/src/components/learner-credit-management/tests/CatalogSearch.test.jsx new file mode 100644 index 0000000000..ee74751e87 --- /dev/null +++ b/src/components/learner-credit-management/tests/CatalogSearch.test.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import { + SEARCH_FACET_FILTERS, + SearchContext, +} from '@edx/frontend-enterprise-catalog-search'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { screen } from '@testing-library/react'; +import { renderWithRouter } from '../../test/testUtils'; +import CatalogSearch from '../search/CatalogSearch'; + +jest.mock('react-instantsearch-dom', () => ({ + ...jest.requireActual('react-instantsearch-dom'), + InstantSearch: () =>
SEARCH
, + Index: () =>
SEARCH
, +})); + +const DEFAULT_SEARCH_CONTEXT_VALUE = { refinements: {} }; + +const SearchDataWrapper = ({ children, searchContextValue }) => ( + + + {children} + + +); + +describe('Catalog Search component', () => { + it('properly renders component', () => { + renderWithRouter( + + + , + ); + expect(screen.getByText('SEARCH')).toBeInTheDocument(); + }); +}); diff --git a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx new file mode 100644 index 0000000000..34a75e3ac0 --- /dev/null +++ b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx @@ -0,0 +1,154 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import { SearchContext } from '@edx/frontend-enterprise-catalog-search'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import { BaseCatalogSearchResults, SKELETON_DATA_TESTID } from '../search/CatalogSearchResults'; + +import { renderWithRouter } from '../../test/testUtils'; + +import { CONTENT_TYPE_COURSE } from '../data/constants'; + +// Mocking this connected component so as not to have to mock the algolia Api +const PAGINATE_ME = 'PAGINATE ME :)'; +const PaginationComponent = () =>
{PAGINATE_ME}
; + +// all we are testing is routes, we don't need InstantSearch to work here +jest.mock('react-instantsearch-dom', () => ({ + ...jest.requireActual('react-instantsearch-dom'), + InstantSearch: () =>
Popular Courses
, + Index: () =>
Popular Courses
, +})); + +const DEFAULT_SEARCH_CONTEXT_VALUE = { refinements: {} }; + +const SearchDataWrapper = ({ + + children, + + searchContextValue = DEFAULT_SEARCH_CONTEXT_VALUE, +}) => ( + + {children} + +); + +const mockConfig = () => ({ + EDX_FOR_BUSINESS_TITLE: 'ayylmao', + EDX_ENTERPRISE_ALACARTE_TITLE: 'baz', + FEATURE_CARD_VIEW_ENABLED: 'True', +}); + +jest.mock('@edx/frontend-platform', () => ({ + ...jest.requireActual('@edx/frontend-platform'), + getConfig: () => mockConfig(), +})); + +const TEST_COURSE_NAME = 'test course'; +const TEST_PARTNER = 'edx'; +const TEST_CATALOGS = ['baz']; + +const TEST_COURSE_NAME_2 = 'test course 2'; +const TEST_PARTNER_2 = 'edx 2'; +const TEST_CATALOGS_2 = ['baz', 'ayylmao']; + +const searchResults = { + nbHits: 2, + hitsPerPage: 10, + pageIndex: 10, + pageCount: 5, + nbPages: 6, + hits: [ + { + title: TEST_COURSE_NAME, + partners: [{ name: TEST_PARTNER, logo_image_url: '' }], + enterprise_catalog_query_titles: TEST_CATALOGS, + card_image_url: 'http://url.test.location', + first_enrollable_paid_seat_price: 100, + original_image_url: '', + availability: ['Available Now'], + content_type: CONTENT_TYPE_COURSE, + advertised_course_run: { + start: '2020-01-24T05:00:00Z', + end: '2080-01-01T17:00:00Z', + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + }, + }, + { + title: TEST_COURSE_NAME_2, + partners: [{ name: TEST_PARTNER_2, logo_image_url: '' }], + enterprise_catalog_query_titles: TEST_CATALOGS_2, + card_image_url: 'http://url.test2.location', + first_enrollable_paid_seat_price: 99, + original_image_url: '', + availability: ['Available Now'], + content_type: CONTENT_TYPE_COURSE, + advertised_course_run: { + start: '2020-01-24T05:00:00Z', + end: '2080-01-01T17:00:00Z', + upgrade_deadline: 1892678399, + pacing_type: 'self_paced', + }, + }, + ], + page: 1, + _state: { disjunctiveFacetsRefinements: { foo: 'bar' } }, +}; + +const defaultProps = { + paginationComponent: PaginationComponent, + searchResults, + isSearchStalled: false, + searchState: { page: 1 }, + error: null, + contentType: CONTENT_TYPE_COURSE, + // mock i18n requirements + intl: { + formatMessage: (header) => header.defaultMessage, + formatDate: () => {}, + formatTime: () => {}, + formatRelative: () => {}, + formatNumber: () => {}, + formatPlural: () => {}, + formatHTMLMessage: () => {}, + now: () => {}, + }, +}; + +describe('Main Catalogs view works as expected', () => { + const OLD_ENV = process.env; + beforeEach(() => { + jest.resetModules(); // Most important - it clears the cache + process.env = { ...OLD_ENV }; // Make a copy + }); + afterEach(() => { + process.env = OLD_ENV; // Restore old environment + }); + + test('all courses rendered when search results available', async () => { + render( + + + + + , + , + ); + expect(screen.queryByText(TEST_COURSE_NAME)).toBeInTheDocument(); + expect(screen.queryByText(TEST_COURSE_NAME_2)).toBeInTheDocument(); + expect(screen.getAllByText('Showing 2 of 2.')[0]).toBeInTheDocument(); + }); + test('isSearchStalled leads to rendering skeleton and not content', () => { + renderWithRouter( + + + , + ); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + expect(screen.queryByText(TEST_COURSE_NAME)).not.toBeInTheDocument(); + expect(screen.getByTestId(SKELETON_DATA_TESTID)).toBeInTheDocument(); + }); +}); diff --git a/src/components/settings/tests/SettingsTabs.test.jsx b/src/components/settings/tests/SettingsTabs.test.jsx index 7dff9d0a67..ed8576396d 100644 --- a/src/components/settings/tests/SettingsTabs.test.jsx +++ b/src/components/settings/tests/SettingsTabs.test.jsx @@ -76,7 +76,6 @@ const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); const defaultStore = getMockStore({ ...initialStore }); -// eslint-disable-next-line react/prop-types const SettingsTabsWithRouter = ({ store = defaultStore }) => ( diff --git a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/EnrollBulkAction.test.jsx b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/EnrollBulkAction.test.jsx index c603c2ebd8..3d42aaa9a2 100644 --- a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/EnrollBulkAction.test.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/EnrollBulkAction.test.jsx @@ -50,7 +50,6 @@ const initialStore = mockStore({ }, }); -// eslint-disable-next-line react/prop-types const EnrollBulkActionWithProvider = ({ store = initialStore, ...rest }) => ( diff --git a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.test.jsx b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.test.jsx index e01e23160d..329cff540b 100644 --- a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.test.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.test.jsx @@ -42,7 +42,6 @@ const initialStore = mockStore({ }, }); -// eslint-disable-next-line react/prop-types const RemindBulkActionWithProvider = ({ store = initialStore, ...rest }) => ( diff --git a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.test.jsx b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.test.jsx index 2e2bbcf624..e5f947b638 100644 --- a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.test.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.test.jsx @@ -39,7 +39,6 @@ const initialStore = mockStore({ }, }); -// eslint-disable-next-line react/prop-types const RevokeBulkActionWithProvider = ({ store = initialStore, ...rest }) => ( diff --git a/src/components/subscriptions/licenses/LicenseManagementTable/tests/index.test.jsx b/src/components/subscriptions/licenses/LicenseManagementTable/tests/index.test.jsx index 6ac5cc5086..de07041417 100644 --- a/src/components/subscriptions/licenses/LicenseManagementTable/tests/index.test.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementTable/tests/index.test.jsx @@ -58,7 +58,6 @@ const expiredSubscriptionPlan = ( }; }; -// eslint-disable-next-line react/prop-types const LicenseManagementTableWrapper = ({ subscriptionPlan, ...props }) => ( diff --git a/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx b/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx index e476bdce5a..7e3633180d 100644 --- a/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx +++ b/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx @@ -63,7 +63,6 @@ const defaultSubscriptions = { const mockStore = configureMockStore([thunk]); -// eslint-disable-next-line react/prop-types const MultipleSubscriptionsPageWrapper = ({ subscriptions = defaultSubscriptions, ...props }) => ( diff --git a/src/components/subscriptions/tests/expiration/SubscriptionExpirationBanner.test.jsx b/src/components/subscriptions/tests/expiration/SubscriptionExpirationBanner.test.jsx index a0dc6aea45..e743f3ae70 100644 --- a/src/components/subscriptions/tests/expiration/SubscriptionExpirationBanner.test.jsx +++ b/src/components/subscriptions/tests/expiration/SubscriptionExpirationBanner.test.jsx @@ -27,7 +27,7 @@ jest.mock('@edx/frontend-enterprise-utils', () => { }); // PropType validation for state is done by SubscriptionManagementContext -// eslint-disable-next-line react/prop-types + const ExpirationBannerWrapper = ({ detailState, isSubscriptionPlanDetails = false }) => ( diff --git a/src/components/subscriptions/tests/expiration/SubscriptionExpirationModals.test.jsx b/src/components/subscriptions/tests/expiration/SubscriptionExpirationModals.test.jsx index 2dbbfe237a..a86f455417 100644 --- a/src/components/subscriptions/tests/expiration/SubscriptionExpirationModals.test.jsx +++ b/src/components/subscriptions/tests/expiration/SubscriptionExpirationModals.test.jsx @@ -29,7 +29,7 @@ jest.mock('@edx/frontend-enterprise-utils', () => { }); // PropType validation for state is done by SubscriptionManagementContext -// eslint-disable-next-line react/prop-types + const ExpirationModalsWithContext = ({ detailState }) => ( diff --git a/src/components/test/testUtils.jsx b/src/components/test/testUtils.jsx index b5ac1bf34e..02a98208fb 100644 --- a/src/components/test/testUtils.jsx +++ b/src/components/test/testUtils.jsx @@ -12,7 +12,6 @@ export function renderWithRouter( history = createMemoryHistory({ initialEntries: [route] }), } = {}, ) { - // eslint-disable-next-line react/prop-types const Wrapper = ({ children }) => ( {children} ); diff --git a/src/containers/EnterpriseApp/EnterpriseApp.test.jsx b/src/containers/EnterpriseApp/EnterpriseApp.test.jsx index 6281ab463d..f10e8b0dd8 100644 --- a/src/containers/EnterpriseApp/EnterpriseApp.test.jsx +++ b/src/containers/EnterpriseApp/EnterpriseApp.test.jsx @@ -47,13 +47,13 @@ const EnterpriseAppContextProvider = ({ jest.mock('../../components/EnterpriseApp/EnterpriseAppContextProvider', () => ({ __esModule: true, ...jest.requireActual('../../components/EnterpriseApp/EnterpriseAppContextProvider'), - // eslint-disable-next-line react/prop-types + default: ({ children }) => {children}, })); jest.mock('../Sidebar', () => ({ __esModule: true, - // eslint-disable-next-line react/prop-types + default: ({ children }) =>
{children}
, })); @@ -94,7 +94,6 @@ const initialState = { dashboardInsights: {}, }; -// eslint-disable-next-line react/prop-types const EnterpriseAppWrapper = ({ store, initialEntries, ...props }) => ( diff --git a/src/data/hooks.js b/src/data/hooks.js index 5f9e7df7b1..c44b512dd5 100644 --- a/src/data/hooks.js +++ b/src/data/hooks.js @@ -1,4 +1,8 @@ -import { useEffect, useRef } from 'react'; +import { + useEffect, useMemo, useState, useRef, +} from 'react'; + +import { CONTENT_TYPE_COURSE } from '../components/learner-credit-management/data/constants'; export function useInterval(callback, delay) { const savedCallback = useRef(); @@ -41,3 +45,12 @@ export function useTimeout(callback, delay) { timeoutIdRef.current = null; }, [callback, delay]); } + +export const useSelectedCourse = () => { + const [course, setCourse] = useState(null); + const isCourse = useMemo( + () => course?.contentType === CONTENT_TYPE_COURSE, + [course], + ); + return [course, setCourse, isCourse]; +}; From cb6b6f89d2716eec449793f8167fa9a303f586d5 Mon Sep 17 00:00:00 2001 From: mahamakifdar19 Date: Thu, 12 Oct 2023 11:49:50 +0500 Subject: [PATCH 038/124] fix: hide track progress tab temporarily due to data issues --- src/components/Admin/AIAnalyticsSummary.jsx | 3 +- .../Admin/AIAnalyticsSummary.test.jsx | 17 +++--- .../Admin/AIAnalyticsSummarySkeleton.jsx | 3 +- .../AIAnalyticsSummary.test.jsx.snap | 60 +------------------ .../Admin/__snapshots__/Admin.test.jsx.snap | 2 +- 5 files changed, 17 insertions(+), 68 deletions(-) diff --git a/src/components/Admin/AIAnalyticsSummary.jsx b/src/components/Admin/AIAnalyticsSummary.jsx index 58ca4c7814..020e18df1a 100644 --- a/src/components/Admin/AIAnalyticsSummary.jsx +++ b/src/components/Admin/AIAnalyticsSummary.jsx @@ -71,9 +71,10 @@ const AIAnalyticsSummary = ({ enterpriseId, insights }) => { + {/* Track Progress is currently hidden due to data inconsistency. It will be addressed as part of ENT-7812 */} -
, - ",", -] -`; - -exports[` should display AnalyticsDetailCard with learner_progress data when Track Progress button is clicked 1`] = ` -Array [ -
- - + )} + {VALIDATED && CONFIGURED && !ENABLED && ( + + )} + + ); + + return ( + + + {renderCardStatusIcon()} + {config.display_name} + {renderCardBadge()} + {renderCardButton()} +
+ )} + subtitle={( +
+ Last modified {convertToReadableDate(config.modified)} +
+ )} + actions={(!SUBMITTED || CONFIGURED) && ( + + + + {VALIDATED && ( + setProviderConfig(config)} + > + Configure + + )} + {(!ENABLED || !VALIDATED) && ( + onDeleteClick(config)} + > + Delete + + )} + {ENABLED && VALIDATED && ( + onDisableClick(config)} + > + Disable + + )} + + + )} + /> + + ); +}; + +NewSSOConfigCard.propTypes = { + config: PropTypes.shape({ + uuid: PropTypes.string, + display_name: PropTypes.string, + active: PropTypes.bool, + modified: PropTypes.string, + validated_at: PropTypes.string, + configured_at: PropTypes.string, + submitted_at: PropTypes.string, + }).isRequired, + setLoading: PropTypes.func.isRequired, + setRefreshBool: PropTypes.func.isRequired, + refreshBool: PropTypes.bool.isRequired, +}; + +export default NewSSOConfigCard; diff --git a/src/components/settings/SettingsSSOTab/hooks.js b/src/components/settings/SettingsSSOTab/hooks.js index 04a2002af8..4069de6804 100644 --- a/src/components/settings/SettingsSSOTab/hooks.js +++ b/src/components/settings/SettingsSSOTab/hooks.js @@ -10,6 +10,7 @@ import { updateIdpDirtyState, } from './data/actions'; import { updateSamlProviderData, deleteSamlProviderData } from './utils'; +import { features } from '../../../config'; const useIdpState = () => { const { @@ -179,23 +180,43 @@ const useExistingSSOConfigs = (enterpriseUuid, refreshBool) => { const [error, setError] = useState(null); useEffect(() => { + const { AUTH0_SELF_SERVICE_INTEGRATION } = features; if (enterpriseUuid) { - const fetchConfig = async () => { - const response = await LmsApiService.getProviderConfig(enterpriseUuid); - return response.data.results; - }; - fetchConfig().then(configs => { - setSsoConfigs(configs); - setLoading(false); - }).catch(err => { - setLoading(false); - if (err.customAttributes?.httpErrorStatus !== 404) { - // nothing found is okay for this fetcher. - setError(err); - } else { - setSsoConfigs([]); - } - }); + if (!AUTH0_SELF_SERVICE_INTEGRATION) { + const fetchConfig = async () => { + const response = await LmsApiService.getProviderConfig(enterpriseUuid); + return response.data.results; + }; + fetchConfig().then(configs => { + setSsoConfigs(configs); + setLoading(false); + }).catch(err => { + setLoading(false); + if (err.customAttributes?.httpErrorStatus !== 404) { + // nothing found is okay for this fetcher. + setError(err); + } else { + setSsoConfigs([]); + } + }); + } else { + const fetchConfig = async () => { + const response = await LmsApiService.listEnterpriseSsoOrchestrationRecords(enterpriseUuid); + return response.data; + }; + fetchConfig().then(orchestratorConfigs => { + setSsoConfigs(orchestratorConfigs); + setLoading(false); + }).catch(err => { + setLoading(false); + if (err.customAttributes?.httpErrorStatus !== 404) { + // nothing found is okay for this fetcher. + setError(err); + } else { + setSsoConfigs([]); + } + }); + } } }, [enterpriseUuid, refreshBool]); diff --git a/src/components/settings/SettingsSSOTab/index.jsx b/src/components/settings/SettingsSSOTab/index.jsx index ff17bfe254..b1181c6817 100644 --- a/src/components/settings/SettingsSSOTab/index.jsx +++ b/src/components/settings/SettingsSSOTab/index.jsx @@ -1,15 +1,18 @@ import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { - Alert, Hyperlink, Toast, Skeleton, + Alert, ActionRow, Button, Hyperlink, ModalDialog, Toast, Skeleton, useToggle, } from '@edx/paragon'; -import { WarningFilled } from '@edx/paragon/icons'; +import { Add, WarningFilled } from '@edx/paragon/icons'; import { HELP_CENTER_SAML_LINK } from '../data/constants'; import { useExistingSSOConfigs, useExistingProviderData } from './hooks'; import NoSSOCard from './NoSSOCard'; import ExistingSSOConfigs from './ExistingSSOConfigs'; +import NewExistingSSOConfigs from './NewExistingSSOConfigs'; import NewSSOConfigForm from './NewSSOConfigForm'; import { SSOConfigContext, SSOConfigContextProvider } from './SSOConfigContext'; +import LmsApiService from '../../../data/services/LmsApiService'; +import { features } from '../../../config'; const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { const { @@ -21,22 +24,133 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { const [existingProviderData, pdError, pdIsLoading] = useExistingProviderData(enterpriseId, refreshBool); const [showNewSSOForm, setShowNewSSOForm] = useState(false); const [showNoSSOCard, setShowNoSSOCard] = useState(false); + const { AUTH0_SELF_SERVICE_INTEGRATION } = features; + const [isOpen, open, close] = useToggle(false); - useEffect(() => { - let validConfigExists = false; - existingConfigs.forEach(config => { - if (config.was_valid_at) { - validConfigExists = true; - } + const newConfigurationButtonOnClick = async () => { + Promise.all(existingConfigs.map(config => LmsApiService.updateEnterpriseSsoOrchestrationRecord( + { active: false, is_removed: true }, + config.uuid, + ))).then(() => { + setRefreshBool(!refreshBool); + close(); }); + }; + + useEffect(() => { + if (AUTH0_SELF_SERVICE_INTEGRATION) { + setHasSSOConfig(existingConfigs.some(config => config.validated_at)); + } else { + setHasSSOConfig(existingConfigs.some(config => config.was_valid_at)); + } if (!existingConfigs || existingConfigs?.length < 1) { setShowNoSSOCard(true); } else { setShowNoSSOCard(false); } - setHasSSOConfig(validConfigExists); - }, [existingConfigs, setHasSSOConfig]); + }, [AUTH0_SELF_SERVICE_INTEGRATION, existingConfigs, setHasSSOConfig]); + if (AUTH0_SELF_SERVICE_INTEGRATION) { + return ( +
+ + + + Create new SSO configuration? + + + +

+ Only one SSO integration is supported at a time.
+
+ To continue updating and editing your SSO integration, select "Cancel" and then + "Configure" on the integration card. Creating a new SSO configuration will overwrite and delete + your existing SSO configuration. +

+
+ + + + Cancel + + + + +
+
+

Single Sign-On (SSO) Integrations

+
+ {existingConfigs?.length > 0 && (providerConfig === null) && ( + + )} + + Help Center: Single Sign-On + +
+
+ {(!isLoading || !pdIsLoading) && ( +
+ {/* providerConfig represents the currently selected config to edit/create, if there are + existing configs but no providerConfig then we can safely render the listings page */} + {existingConfigs?.length > 0 && (providerConfig === null) && ( + + )} + {/* Nothing found so guide user to creation/edit form */} + {showNoSSOCard && } + {/* Since we found a selected providerConfig we know we are in editing mode and can safely + render the create/edit form */} + {((existingConfigs?.length > 0 && providerConfig !== null) || showNewSSOForm) && ()} + {error && ( + + An error occurred loading the SAML configs:

{error?.message}

+
+ )} + {pdError && ( + + An error occurred loading the SAML data:

{pdError?.message}

+
+ )} + {infoMessage && ( + setInfoMessage(null)} + show={infoMessage.length > 0} + > + {infoMessage} + + )} +
+ )} + {(isLoading || pdIsLoading) && } +
+ ); + } return (
diff --git a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx new file mode 100644 index 0000000000..cd0b7dfdd1 --- /dev/null +++ b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx @@ -0,0 +1,245 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import { + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; +import userEvent from '@testing-library/user-event'; +import { + act, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import { Provider } from 'react-redux'; +import { getMockStore, enterpriseId } from '../testutils'; +import { features } from '../../../../config'; +import NewExistingSSOConfigs from '../NewExistingSSOConfigs'; +import { SSOConfigContext, SSO_INITIAL_STATE } from '../SSOConfigContext'; +import LmsApiService from '../../../../data/services/LmsApiService'; + +const queryClient = new QueryClient({ + queries: { + retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis. + }, +}); + +jest.mock('../../utils'); +jest.mock('../../../../data/services/LmsApiService'); +const mockSetRefreshBool = jest.fn(); + +const initialStore = { + portalConfiguration: { + enterpriseId, + enterpriseSlug: 'sluggy', + enterpriseName: 'sluggyent', + contactEmail: 'foobar', + }, +}; +const store = getMockStore({ contactEmail: 'foobar', ...initialStore }); +const inactiveConfig = [ + { + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: false, + modified: '2022-04-12T19:51:25Z', + configured_at: '2022-05-12T19:51:25Z', + validated_at: '2022-06-12T19:51:25Z', + submitted_at: '2022-04-12T19:51:25Z', + }, +]; +const activeConfig = [ + { + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: true, + modified: '2022-04-12T19:51:25Z', + configured_at: '2022-05-12T19:51:25Z', + validated_at: '2022-06-12T19:51:25Z', + submitted_at: '2022-04-12T19:51:25Z', + }, +]; +const unvalidatedConfig = [ + { + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: true, + modified: '2022-04-12T19:51:25Z', + configured_at: '2022-04-12T19:51:25Z', + validated_at: null, + submitted_at: '2022-04-12T19:51:25Z', + }, +]; +const inProgressConfig = [ + { + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: false, + modified: '2022-04-12T19:51:25Z', + configured_at: '2021-04-12T19:51:25Z', + validated_at: null, + submitted_at: '2022-04-12T19:51:25Z', + }, +]; +const notConfiguredConfig = [ + { + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: false, + modified: '2022-04-12T19:51:25Z', + configured_at: null, + validated_at: null, + submitted_at: '2022-04-12T19:51:25Z', + }, +]; + +jest.mock('../data/actions'); +jest.mock('../../utils'); +const entryType = 'direct'; +const metadataURL = 'https://foobar.com'; +const entityID = 'foobar'; +const publicKey = 'abc123'; +const ssoUrl = 'https://foobar.com'; +const mockCreateOrUpdateIdpRecord = jest.fn(); +const mockHandleEntityIDUpdate = jest.fn(); +const mockHandleMetadataEntryTypeUpdate = jest.fn(); +jest.mock('../hooks', () => { + const originalModule = jest.requireActual('../hooks'); + return { + ...originalModule, + useIdpState: () => ({ + entryType, + metadataURL, + entityID, + publicKey, + ssoUrl, + createOrUpdateIdpRecord: mockCreateOrUpdateIdpRecord, + handleEntityIDUpdate: mockHandleEntityIDUpdate, + handleMetadataEntryTypeUpdate: mockHandleMetadataEntryTypeUpdate, + }), + useExistingSSOConfigs: () => [[{ hehe: 'haha' }], null, true], + }; +}); + +const mockSetProviderConfig = jest.fn(); +const contextValue = { + ...SSO_INITIAL_STATE, + setCurrentError: jest.fn(), + currentError: null, + dispatchSsoState: jest.fn(), + ssoState: { + idp: { + metadataURL: '', + entityID: '', + entryType: '', + isDirty: false, + }, + serviceprovider: { + isSPConfigured: false, + }, + refreshBool: false, + providerConfig: { + id: 1337, + }, + }, + setProviderConfig: mockSetProviderConfig, + setRefreshBool: jest.fn(), +}; + +const setupNewExistingSSOConfigs = (configs) => { + features.AUTH0_SELF_SERVICE_INTEGRATION = true; + return render( + + + + + + + + + , + ); +}; + +describe('New Existing SSO Configs tests', () => { + afterEach(() => { + features.AUTH0_SELF_SERVICE_INTEGRATION = false; + jest.clearAllMocks(); + }); + test('checks and sets in progress configs', async () => { + setupNewExistingSSOConfigs(inProgressConfig); + expect( + screen.queryByText( + 'Your SSO Integration is in progress', + ), + ).toBeInTheDocument(); + }); + test('checks and sets not configured configs', async () => { + setupNewExistingSSOConfigs(notConfiguredConfig); + expect( + screen.queryByText( + 'Your SSO Integration is in progress', + ), + ).toBeInTheDocument(); + }); + test('checks and sets validated configs', async () => { + setupNewExistingSSOConfigs(activeConfig); + expect( + screen.queryByText( + 'Your SSO integration is live!', + ), + ).toBeInTheDocument(); + }); + test('checks and sets un-validated configs', async () => { + setupNewExistingSSOConfigs(unvalidatedConfig); + expect( + screen.queryByText( + 'You need to test your SSO connection', + ), + ).toBeInTheDocument(); + }); + test('polls for finished configs', async () => { + const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords'); + spy.mockImplementation(() => Promise.resolve({ + data: [{ + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: true, + modified: '2022-04-12T19:51:25Z', + configured_at: '2022-05-12T19:51:25Z', + validated_at: '2022-06-12T19:51:25Z', + submitted_at: '2022-04-12T19:51:25Z', + }], + })); + setupNewExistingSSOConfigs(inProgressConfig); + expect( + screen.queryByText( + 'Your SSO Integration is in progress', + ), + ).toBeInTheDocument(); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + expect(mockSetRefreshBool).toHaveBeenCalledTimes(2); + }); + test('enabling config sets loading and renders skeleton', async () => { + const spy = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord'); + spy.mockImplementation(() => Promise.resolve({})); + setupNewExistingSSOConfigs(inactiveConfig); + const button = screen.getByTestId('existing-sso-config-card-enable-button'); + act(() => { + userEvent.click(button); + }); + expect(spy).toBeCalledTimes(1); + await waitFor(() => expect( + screen.queryByTestId( + 'sso-self-service-skeleton', + ), + ).toBeInTheDocument()); + }); +}); diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx new file mode 100644 index 0000000000..c62c7972ac --- /dev/null +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx @@ -0,0 +1,165 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { Provider } from 'react-redux'; +import { render, screen, waitFor } from '@testing-library/react'; +import { SSOConfigContext, SSO_INITIAL_STATE } from '../SSOConfigContext'; +import { getMockStore, initialStore } from '../testutils'; +import NewSSOConfigAlerts from '../NewSSOConfigAlerts'; + +const store = getMockStore({ contactEmail: 'foobar', ...initialStore }); +const mockSetProviderConfig = jest.fn(); +const contextValue = { + ...SSO_INITIAL_STATE, + setCurrentError: jest.fn(), + currentError: null, + dispatchSsoState: jest.fn(), + ssoState: { + idp: { + metadataURL: '', + entityID: '', + entryType: '', + isDirty: false, + }, + serviceprovider: { + isSPConfigured: false, + }, + refreshBool: false, + providerConfig: { + id: 1337, + }, + }, + setProviderConfig: mockSetProviderConfig, + setRefreshBool: jest.fn(), +}; + +describe('New SSO Config Alerts Tests', () => { + test('displays inProgress alert properly', async () => { + render( + + + + , + + + , + ); + expect( + screen.queryByText( + 'Your SSO Integration is in progress', + ), + ).toBeInTheDocument(); + expect( + screen.queryByText( + 'You need to test your SSO connection', + ), + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + 'Your SSO integration is live!', + ), + ).not.toBeInTheDocument(); + }); + test('inProgress alert accounts for if configured before', () => { + render( + + + + , + + + , + ); + expect( + screen.getByText( + 'five minutes', + { exact: false }, + ), + ).toBeInTheDocument(); + }); + test('displays untested alert properly', () => { + render( + + + + , + + + , + ); + expect( + screen.queryByText( + 'You need to test your SSO connection', + ), + ).toBeInTheDocument(); + expect( + screen.queryByText( + 'Your SSO integration is live!', + ), + ).not.toBeInTheDocument(); + }); + test('displays live alert properly', () => { + render( + + + + , + + + , + ); + expect( + screen.queryByText( + 'Your SSO integration is live!', + ), + ).toBeInTheDocument(); + }); + test('calls closeAlerts prop on close', async () => { + const mockCloseAlerts = jest.fn(); + render( + + + + , + + + , + ); + await waitFor(() => { + userEvent.click(screen.getByText('Dismiss')); + }, []).then(() => { + expect(mockCloseAlerts).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx new file mode 100644 index 0000000000..a953ecb752 --- /dev/null +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx @@ -0,0 +1,222 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import userEvent from '@testing-library/user-event'; +import { act, render, screen } from '@testing-library/react'; +import NewSSOConfigCard from '../NewSSOConfigCard'; +import LmsApiService from '../../../../data/services/LmsApiService'; + +describe('New SSO Config Card Tests', () => { + test('displays enabled and validated status icon properly', async () => { + render( + , + ); + expect( + screen.getByTestId( + 'existing-sso-config-card-enabled-icon', + ), + ).toBeInTheDocument(); + }); + test('displays not validated status icon properly', async () => { + render( + , + ); + expect( + screen.getByTestId( + 'existing-sso-config-card-not-validated-icon', + ), + ).toBeInTheDocument(); + }); + test('displays not validated status icon properly', async () => { + render( + , + ); + expect( + screen.getByTestId( + 'existing-sso-config-card-not-active-icon', + ), + ).toBeInTheDocument(); + }); + test('displays badges properly', async () => { + render( + , + ); + expect( + screen.getByTestId( + 'existing-sso-config-card-badge-in-progress', + ), + ).toBeInTheDocument(); + render( + , + ); + expect( + screen.getByTestId( + 'existing-sso-config-card-badge-disabled', + ), + ).toBeInTheDocument(); + }); + test('displays configure button properly', async () => { + render( + , + ); + expect( + screen.getByTestId( + 'existing-sso-config-card-configure-button', + ), + ).toBeInTheDocument(); + }); + test('displays enable button properly', async () => { + render( + , + ); + expect( + screen.getByTestId( + 'existing-sso-config-card-enable-button', + ), + ).toBeInTheDocument(); + }); + test('handles kebob Delete dropdown option', async () => { + const spy = jest.spyOn(LmsApiService, 'deleteEnterpriseSsoOrchestrationRecord'); + spy.mockImplementation(() => Promise.resolve({})); + render( + , + ); + act(() => { + userEvent.click(screen.getByTestId('existing-sso-config-card-dropdown')); + }); + act(() => { + userEvent.click(screen.getByTestId('existing-sso-config-delete-dropdown')); + }); + expect(spy).toBeCalledTimes(1); + }); + test('handles kebob Disable dropdown option', async () => { + const spy = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord'); + spy.mockImplementation(() => Promise.resolve({})); + render( + , + ); + act(() => { + userEvent.click(screen.getByTestId('existing-sso-config-card-dropdown')); + }); + act(() => { + userEvent.click(screen.getByTestId('existing-sso-config-disable-dropdown')); + }); + expect(spy).toBeCalledTimes(1); + }); +}); diff --git a/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx b/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx index 4a05d2b026..a46e4697d8 100644 --- a/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx @@ -1,12 +1,18 @@ import { act, render, screen, waitFor, } from '@testing-library/react'; +import { + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; import '@testing-library/jest-dom/extend-expect'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { Provider } from 'react-redux'; import { HELP_CENTER_SAML_LINK } from '../../data/constants'; +import { features } from '../../../../config'; import SettingsSSOTab from '..'; import LmsApiService from '../../../../data/services/LmsApiService'; @@ -19,14 +25,24 @@ const initialStore = { enterpriseId, enterpriseSlug: 'sluggy', enterpriseName: 'sluggyent', + contactEmail: 'foobar', }, }; +const queryClient = new QueryClient({ + queries: { + retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis. + }, +}); const mockStore = configureMockStore([thunk]); const getMockStore = aStore => mockStore(aStore); const store = getMockStore({ ...initialStore }); describe('SAML Config Tab', () => { + afterEach(() => { + features.AUTH0_SELF_SERVICE_INTEGRATION = false; + jest.clearAllMocks(); + }); test('renders base page with correct text and help center link', async () => { const aResult = () => Promise.resolve(1); LmsApiService.getProviderConfig.mockImplementation(() => ( @@ -57,7 +73,7 @@ describe('SAML Config Tab', () => { () => expect(mockSetHasSSOConfig).toBeCalledWith(false), ); }); - test('page sets has valid sso config with valid configs ', async () => { + test('page sets has valid sso config with valid configs', async () => { LmsApiService.getProviderConfig.mockImplementation(() => ( { data: { results: [{ was_valid_at: '10/10/22' }] } } )); @@ -70,4 +86,31 @@ describe('SAML Config Tab', () => { () => expect(mockSetHasSSOConfig).toBeCalledWith(true), ); }); + test('page renders new sso self service tool properly', async () => { + features.AUTH0_SELF_SERVICE_INTEGRATION = true; + const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords'); + spy.mockImplementation(() => Promise.resolve({ + data: [{ + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: true, + modified: '2022-04-12T19:51:25Z', + configured_at: '2022-05-12T19:51:25Z', + validated_at: '2022-06-12T19:51:25Z', + submitted_at: '2022-04-12T19:51:25Z', + }], + })); + await waitFor(() => render( + + + + + , + + , + )); + expect(screen.queryByText( + 'Great news! Your test was successful and your new SSO integration is live and ready to use.', + )).toBeInTheDocument(); + }); }); diff --git a/src/components/settings/SettingsTabs.jsx b/src/components/settings/SettingsTabs.jsx index 8f471233c3..729ac106bc 100644 --- a/src/components/settings/SettingsTabs.jsx +++ b/src/components/settings/SettingsTabs.jsx @@ -1,4 +1,8 @@ import React, { useState, useMemo } from 'react'; +import { + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; import { Container, Tabs, @@ -27,6 +31,12 @@ import SettingsApiCredentialsTab from './SettingsApiCredentialsTab'; import { features } from '../../config'; import { updatePortalConfigurationEvent } from '../../data/actions/portalConfiguration'; +const queryClient = new QueryClient({ + queries: { + retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis. + }, +}); + const SettingsTabs = ({ enterpriseId, enterpriseSlug, @@ -80,10 +90,12 @@ const SettingsTabs = ({ eventKey={SETTINGS_TABS_VALUES.sso} title={SETTINGS_TAB_LABELS.sso} > - + + + , ); } diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js index 18f2cca502..b3c32f662d 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -46,7 +46,7 @@ class LmsApiService { return LmsApiService.apiClient().get(enterpriseSsoOrchestrationFetchUrl); } - static listEnterpriseSsoOrchestration(enterpriseCustomerUuid) { + static listEnterpriseSsoOrchestrationRecords(enterpriseCustomerUuid) { const enterpriseSsoOrchestrationListUrl = `${LmsApiService.enterpriseSsoOrchestrationUrl}`; if (enterpriseCustomerUuid) { return LmsApiService.apiClient().get(`${enterpriseSsoOrchestrationListUrl}?enterprise_customer=${enterpriseCustomerUuid}`); From 03e4c8dbe51194b795a3a3dedc21cad3cddaf85b Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Tue, 17 Oct 2023 18:44:10 +0000 Subject: [PATCH 041/124] feat: putting live sso config alert behind a cookie --- .../SettingsSSOTab/NewSSOConfigAlerts.jsx | 142 ++++++++++-------- .../SettingsSSOTab/NewSSOConfigCard.jsx | 29 ++-- .../tests/NewSSOConfigAlerts.test.jsx | 48 +++++- .../tests/NewSSOConfigCard.test.jsx | 12 +- 4 files changed, 149 insertions(+), 82 deletions(-) diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx index 01fd8e047e..26f1e24c05 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx @@ -5,6 +5,11 @@ import { CheckCircle, Warning, } from '@edx/paragon/icons'; import { Alert } from '@edx/paragon'; +import Cookies from 'universal-cookie'; + +export const SSO_SETUP_COMPLETION_COOKIE_NAME = 'dismissed-sso-completion-alert'; +const SSO_ALERT_OVERRIDE_PARAM = 'sso_alert_override'; +const ssoCookies = new Cookies(); const NewSSOConfigAlerts = ({ inProgressConfigs, @@ -13,66 +18,83 @@ const NewSSOConfigAlerts = ({ notConfigured, contactEmail, closeAlerts, -}) => ( - <> - {inProgressConfigs.length >= 1 && ( - - Your SSO Integration is in progress -

- edX is configuring your SSO. This step takes approximately{' '} - {notConfigured.length > 0 ? `five minutes. You will receive an email at ${contactEmail} when the configuration is complete` : 'fifteen seconds'}. -

-
- )} - {untestedConfigs.length >= 1 && inProgressConfigs.length === 0 && ( - - You need to test your SSO connection -

- Your SSO configuration has completed, - and you should have received an email with the following instructions:
-
- 1. Copy the URL for your learner Portal dashboard below:
-
-   http://courses.edx.org/dashboard?tpa_hint=saml-bestrun-hana
-
- 2: Launch a new incognito or private window and paste the copied URL into the URL bar to load your - learner Portal dashboard.
-
- 3: When prompted, enter login credentials supported by your IDP to test your connection to edX.
-
- Return to this window after completing the testing instructions. - This window will automatically update when a successful test is detected.
-

-
- )} - {liveConfigs.length >= 1 && inProgressConfigs.length === 0 && untestedConfigs.length === 0 && ( - - Your SSO integration is live! -

- Great news! Your test was successful and your new SSO integration is live and ready to use. -

-
- )} - -); +}) => { + const dismissSetupCompleteAlert = () => { + ssoCookies.set( + SSO_SETUP_COMPLETION_COOKIE_NAME, + true, + { sameSite: 'strict' }, + ); + closeAlerts(); + }; + + const searchParams = new URLSearchParams(window.location.search); + const dismissedSSOSetupCompletionCookie = ssoCookies.get(SSO_SETUP_COMPLETION_COOKIE_NAME) === 'true'; + const hideSSOLiveAlert = dismissedSSOSetupCompletionCookie && !searchParams.get(SSO_ALERT_OVERRIDE_PARAM); + return ( + <> + {inProgressConfigs.length >= 1 && ( + + Your SSO Integration is in progress +

+ edX is configuring your SSO. This step takes approximately{' '} + {notConfigured.length > 0 ? `five minutes. You will receive an email at ${contactEmail} when the configuration is complete` : 'fifteen seconds'}. +

+
+ )} + {untestedConfigs.length >= 1 && inProgressConfigs.length === 0 && ( + + You need to test your SSO connection +

+ Your SSO configuration has completed, + and you should have received an email with the following instructions:
+
+ 1. Copy the URL for your learner Portal dashboard below:
+
+   http://courses.edx.org/dashboard?tpa_hint=saml-bestrun-hana
+
+ 2: Launch a new incognito or private window and paste the copied URL into the URL bar to load your + learner Portal dashboard.
+
+ 3: When prompted, enter login credentials supported by your IDP to test your connection to edX.
+
+ Return to this window after completing the testing instructions. + This window will automatically update when a successful test is detected.
+

+
+ )} + {liveConfigs.length >= 1 && ( + inProgressConfigs.length === 0) && ( + untestedConfigs.length === 0) && ( + !hideSSOLiveAlert) && ( + + Your SSO integration is live! +

+ Great news! Your test was successful and your new SSO integration is live and ready to use. +

+
+ )} + + ); +}; NewSSOConfigAlerts.propTypes = { inProgressConfigs: PropTypes.arrayOf(PropTypes.shape({})).isRequired, diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx index 9e83ef4b21..e7f94d0239 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx @@ -49,9 +49,16 @@ const NewSSOConfigCard = ({ }); }; + const renderKeyOffIcon = (dataTestId) => ( + + ); + const renderCardStatusIcon = () => ( <> - {VALIDATED && ENABLED && ( + {VALIDATED && ENABLED && CONFIGURED && ( )} - {!VALIDATED && ( + {!VALIDATED && CONFIGURED && ( This integration has not been validated. Please follow the testing instructions to validate your integration. - )} + } > - + {renderKeyOffIcon('existing-sso-config-card-off-not-validated-icon')} )} - {VALIDATED && !ENABLED && ( - + {(!ENABLED || !CONFIGURED) && ( + <> + {renderKeyOffIcon('existing-sso-config-card-off-icon')} + )} ); diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx index c62c7972ac..47c79f90fe 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx @@ -5,10 +5,19 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import { Provider } from 'react-redux'; import { render, screen, waitFor } from '@testing-library/react'; import { SSOConfigContext, SSO_INITIAL_STATE } from '../SSOConfigContext'; -import { getMockStore, initialStore } from '../testutils'; -import NewSSOConfigAlerts from '../NewSSOConfigAlerts'; +import { getMockStore } from '../testutils'; +import NewSSOConfigAlerts, { SSO_SETUP_COMPLETION_COOKIE_NAME } from '../NewSSOConfigAlerts'; -const store = getMockStore({ contactEmail: 'foobar', ...initialStore }); +const enterpriseId = 'an-id-1'; +const initialStore = { + portalConfiguration: { + enterpriseId, + enterpriseSlug: 'sluggy', + enterpriseName: 'sluggyent', + contactEmail: 'foobar', + }, +}; +const store = getMockStore({ ...initialStore }); const mockSetProviderConfig = jest.fn(); const contextValue = { ...SSO_INITIAL_STATE, @@ -35,6 +44,9 @@ const contextValue = { }; describe('New SSO Config Alerts Tests', () => { + afterEach(() => { + jest.resetAllMocks(); + }); test('displays inProgress alert properly', async () => { render( @@ -118,6 +130,10 @@ describe('New SSO Config Alerts Tests', () => { ).not.toBeInTheDocument(); }); test('displays live alert properly', () => { + Object.defineProperty(window.document, 'cookie', { + writable: true, + value: `${SSO_SETUP_COMPLETION_COOKIE_NAME}=false`, + }); render( @@ -162,4 +178,30 @@ describe('New SSO Config Alerts Tests', () => { expect(mockCloseAlerts).toHaveBeenCalled(); }); }); + test('hides live alert properly after dismissing', () => { + Object.defineProperty(window.document, 'cookie', { + writable: true, + value: `${SSO_SETUP_COMPLETION_COOKIE_NAME}=true`, + }); + render( + + + + , + + + , + ); + expect( + screen.queryByText( + 'Your SSO integration is live!', + ), + ).not.toBeInTheDocument(); + }); }); diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx index a953ecb752..07398e464a 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigCard.test.jsx @@ -14,8 +14,8 @@ describe('New SSO Config Card Tests', () => { uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', active: true, modified: '2021-08-05T15:00:00Z', - validated_at: '2021-08-05T15:00:00Z', - configured_at: '2021-08-05T15:00:00Z', + validated_at: '2021-08-07T15:00:00Z', + configured_at: '2021-08-06T15:00:00Z', submitted_at: '2021-08-05T15:00:00Z', }} setLoading={jest.fn()} @@ -39,7 +39,7 @@ describe('New SSO Config Card Tests', () => { modified: '2021-08-05T15:00:00Z', validated_at: null, configured_at: '2021-08-05T15:00:00Z', - submitted_at: '2021-08-05T15:00:00Z', + submitted_at: '2021-08-04T15:00:00Z', }} setLoading={jest.fn()} setRefreshBool={jest.fn()} @@ -48,11 +48,11 @@ describe('New SSO Config Card Tests', () => { ); expect( screen.getByTestId( - 'existing-sso-config-card-not-validated-icon', + 'existing-sso-config-card-off-not-validated-icon', ), ).toBeInTheDocument(); }); - test('displays not validated status icon properly', async () => { + test('displays key off icon status icon properly', async () => { render( { ); expect( screen.getByTestId( - 'existing-sso-config-card-not-active-icon', + 'existing-sso-config-card-off-icon', ), ).toBeInTheDocument(); }); From ba7c4a89b4a08a56975cca24234edaa1e9cc439b Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Thu, 19 Oct 2023 11:55:38 -0400 Subject: [PATCH 042/124] feat: add assigned table to LCM activity tab (#1054) --- .env.development | 2 +- package-lock.json | 100 ++++++- package.json | 5 +- .../AIAnalyticsSummary.test.jsx.snap | 12 +- .../Admin/__snapshots__/Admin.test.jsx.snap | 206 +++++++-------- src/components/App/index.jsx | 135 ++++++---- .../ManageCodesTab.test.jsx.snap | 33 ++- .../CodeSearchResults.test.jsx.snap | 26 +- .../CompletedLearnersTable.test.jsx.snap | 2 +- .../Coupon/__snapshots__/Coupon.test.jsx.snap | 8 +- ...rnersForInactiveCoursesTable.test.jsx.snap | 6 +- .../EnrolledLearnersTable.test.jsx.snap | 2 +- .../EnrollmentsTable.test.jsx.snap | 2 +- .../__snapshots__/ErrorPage.test.jsx.snap | 20 +- .../__snapshots__/ForbiddenPage.test.jsx.snap | 18 +- .../LearnerActivityTable.test.jsx.snap | 14 +- .../__snapshots__/NumberCard.test.jsx.snap | 6 +- .../PastWeekPassedLearnersTable.test.jsx.snap | 4 +- .../ReduxFormCheckbox.test.jsx.snap | 2 + .../RegisteredLearnersTable.test.jsx.snap | 2 +- .../__snapshots__/SearchBar.test.jsx.snap | 2 +- src/components/Sidebar/index.jsx | 18 +- ...ubsidyRequestManagementTable.test.jsx.snap | 4 +- .../AssignmentDetailsTableCell.jsx | 47 ++++ .../BudgetAssignmentsTable.jsx | 63 +++++ .../BudgetDetailActivityTabContents.jsx | 63 +++-- .../BudgetDetailAssignments.jsx | 38 +++ .../BudgetDetailPage.jsx | 79 ++---- .../BudgetDetailPageHeader.jsx | 50 ++++ .../BudgetDetailPageWrapper.jsx | 31 +++ .../BudgetDetailRedemptions.jsx | 57 ++++ .../BudgetDetailTabsAndRoutes.jsx | 30 ++- ...tate.jsx => CustomDataTableEmptyState.jsx} | 4 +- .../EmailAddressTableCell.jsx | 71 +++-- .../LearnerCreditAllocationTable.jsx | 128 ++++----- .../SpendTableEnrollmentDetails.jsx | 14 +- .../data/constants.js | 5 +- .../data/hooks/hooks.js | 13 - .../data/hooks/index.js | 3 + .../data/hooks/useBudgetContentAssignments.js | 65 +++++ .../hooks/useBudgetContentAssignments.test.js | 131 ++++++++++ .../data/hooks/useBudgetDetailTabs.jsx | 5 +- .../data/hooks/useBudgetId.js | 25 ++ .../useOfferRedemptions.test.js} | 66 +---- .../data/hooks/useOfferSummary.test.js | 67 +++++ .../data/hooks/useSubsidyAccessPolicy.js | 32 +++ .../hooks/useSubsidyAccessPolicy.test.jsx | 121 +++++++++ .../learner-credit-management/data/utils.js | 3 +- .../search/CatalogSearch.jsx | 2 +- .../search/CatalogSearchResults.jsx | 5 - .../tests/BudgetDetailPage.test.jsx | 247 ++++++++++++------ .../tests/EmailAddressTableCell.test.jsx | 77 +++++- .../tests/MultipleBudgetsPage.test.jsx | 5 +- src/components/settings/SettingsTabs.jsx | 21 +- src/components/test/testUtils.jsx | 2 +- .../__snapshots__/Sidebar.test.jsx.snap | 52 ++-- src/data/hooks.js | 15 +- .../services/EnterpriseAccessApiService.js | 25 ++ .../tests/EnterpriseAccessApiService.test.js | 21 ++ 59 files changed, 1627 insertions(+), 685 deletions(-) create mode 100644 src/components/learner-credit-management/AssignmentDetailsTableCell.jsx create mode 100644 src/components/learner-credit-management/BudgetAssignmentsTable.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailAssignments.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailPageHeader.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailPageWrapper.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailRedemptions.jsx rename src/components/learner-credit-management/{SpendTableEmptyState.jsx => CustomDataTableEmptyState.jsx} (75%) delete mode 100644 src/components/learner-credit-management/data/hooks/hooks.js create mode 100644 src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js create mode 100644 src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js create mode 100644 src/components/learner-credit-management/data/hooks/useBudgetId.js rename src/components/learner-credit-management/data/{tests/hooks.test.js => hooks/useOfferRedemptions.test.js} (60%) create mode 100644 src/components/learner-credit-management/data/hooks/useOfferSummary.test.js create mode 100644 src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js create mode 100644 src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx diff --git a/.env.development b/.env.development index 8a4494712a..92e64b6562 100644 --- a/.env.development +++ b/.env.development @@ -41,6 +41,7 @@ FEATURE_SETTINGS_PAGE_APPEARANCE_TAB='true' FEATURE_LEARNER_CREDIT_MANAGEMENT='true' FEATURE_CONTENT_HIGHLIGHTS='true' FEATURE_API_CREDENTIALS_TAB='true' +FEATURE_SSO_SETTINGS_TAB='true' HOTJAR_APP_ID='' HOTJAR_VERSION='6' HOTJAR_DEBUG='' @@ -52,4 +53,3 @@ USE_API_CACHE='true' SUBSCRIPTION_LPR='true' PLOTLY_SERVER_URL='http://localhost:8050' AUTH0_SELF_SERVICE_INTEGRATION='true' -FEATURE_SSO_SETTINGS_TAB='true' diff --git a/package-lock.json b/package-lock.json index fe42c17d03..d37f4b5d06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,9 @@ "@edx/frontend-enterprise-logistration": "3.2.0", "@edx/frontend-enterprise-utils": "3.2.0", "@edx/frontend-platform": "4.0.1", - "@edx/paragon": "20.39.2", - "@tanstack/react-query": "^4.35.7", + "@edx/paragon": "20.46.3", + "@tanstack/react-query": "4.36.1", + "@tanstack/react-query-devtools": "4.36.1", "algoliasearch": "4.8.3", "axios-mock-adapter": "1.19.0", "classnames": "2.2.6", @@ -3594,9 +3595,9 @@ } }, "node_modules/@edx/paragon": { - "version": "20.39.2", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.39.2.tgz", - "integrity": "sha512-SvJskMG+hjRAoteR+dhjXIFAAgMk4IgnDA4gvBomxOk/D+zV+E3mebEoslX2Qx+krRLSwmfQLWhWYn4qlps/5w==", + "version": "20.46.3", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.3.tgz", + "integrity": "sha512-cHxoxoOREVFbBqW9IRAtlIAQo1lcF9JJXkLoEw1Vam6oetKSa5Mc0SL5kykbV+1iRPP7kS8A0Csf5nRr0oolLQ==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", @@ -5554,21 +5555,36 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", + "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, "node_modules/@tanstack/query-core": { - "version": "4.35.7", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.35.7.tgz", - "integrity": "sha512-PgDJtX75ubFS0WCYFM7DqEoJ4QbxU3S5OH3gJSI40xr7UVVax3/J4CM3XUMOTs+EOT5YGEfssi3tfRVGte4DEw==", + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "4.35.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.35.7.tgz", - "integrity": "sha512-0MankquP/6EOM2ATfEov6ViiKemey5uTbjGlFMX1xGotwNaqC76YKDMJdHumZupPbZcZPWAeoPGEHQmVKIKoOQ==", + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", "dependencies": { - "@tanstack/query-core": "4.35.7", + "@tanstack/query-core": "4.36.1", "use-sync-external-store": "^1.2.0" }, "funding": { @@ -5589,6 +5605,25 @@ } } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz", + "integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^4.36.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", @@ -8961,6 +8996,20 @@ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -13701,6 +13750,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", + "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -19779,6 +19839,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -21695,6 +21760,17 @@ "node": ">= 0.12" } }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index fbd1b8ffac..324366e66f 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,9 @@ "@edx/frontend-enterprise-logistration": "3.2.0", "@edx/frontend-enterprise-utils": "3.2.0", "@edx/frontend-platform": "4.0.1", - "@edx/paragon": "20.39.2", - "@tanstack/react-query": "^4.35.7", + "@edx/paragon": "20.46.3", + "@tanstack/react-query": "4.36.1", + "@tanstack/react-query-devtools": "4.36.1", "algoliasearch": "4.8.3", "axios-mock-adapter": "1.19.0", "classnames": "2.2.6", diff --git a/src/components/Admin/__snapshots__/AIAnalyticsSummary.test.jsx.snap b/src/components/Admin/__snapshots__/AIAnalyticsSummary.test.jsx.snap index 73d186b0e3..ff111de353 100644 --- a/src/components/Admin/__snapshots__/AIAnalyticsSummary.test.jsx.snap +++ b/src/components/Admin/__snapshots__/AIAnalyticsSummary.test.jsx.snap @@ -21,7 +21,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -43,7 +43,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -75,7 +75,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -97,7 +97,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -129,7 +129,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -151,7 +151,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap index be393984b1..c74ac78cdd 100644 --- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap +++ b/src/components/Admin/__snapshots__/Admin.test.jsx.snap @@ -246,7 +246,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -297,7 +297,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -367,7 +367,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -418,7 +418,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -555,7 +555,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -657,7 +657,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -708,7 +708,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -821,7 +821,7 @@ exports[` renders correctly with dashboard analytics data renders # cou xmlns="http://www.w3.org/2000/svg" > @@ -966,7 +966,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1017,7 +1017,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1087,7 +1087,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1138,7 +1138,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1275,7 +1275,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1377,7 +1377,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1428,7 +1428,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1541,7 +1541,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1686,7 +1686,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1737,7 +1737,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1807,7 +1807,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1858,7 +1858,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -1995,7 +1995,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -2097,7 +2097,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -2148,7 +2148,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -2261,7 +2261,7 @@ exports[` renders correctly with dashboard analytics data renders # of xmlns="http://www.w3.org/2000/svg" > @@ -2410,7 +2410,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -2461,7 +2461,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -2531,7 +2531,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -2582,7 +2582,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -2719,7 +2719,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -2821,7 +2821,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -2872,7 +2872,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -3087,7 +3087,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -3175,7 +3175,7 @@ exports[` renders correctly with dashboard analytics data renders colla xmlns="http://www.w3.org/2000/svg" > @@ -3277,7 +3277,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -3328,7 +3328,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -3398,7 +3398,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -3449,7 +3449,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -3586,7 +3586,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -3688,7 +3688,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -3739,7 +3739,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -3954,7 +3954,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -4042,7 +4042,7 @@ exports[` renders correctly with dashboard analytics data renders full xmlns="http://www.w3.org/2000/svg" > @@ -4144,7 +4144,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4195,7 +4195,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4265,7 +4265,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4316,7 +4316,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4453,7 +4453,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4555,7 +4555,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4606,7 +4606,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4719,7 +4719,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4868,7 +4868,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4919,7 +4919,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -4989,7 +4989,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -5040,7 +5040,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -5177,7 +5177,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -5279,7 +5279,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -5330,7 +5330,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -5443,7 +5443,7 @@ exports[` renders correctly with dashboard analytics data renders inact xmlns="http://www.w3.org/2000/svg" > @@ -5592,7 +5592,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -5643,7 +5643,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -5713,7 +5713,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -5764,7 +5764,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -5901,7 +5901,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -6003,7 +6003,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -6054,7 +6054,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -6167,7 +6167,7 @@ exports[` renders correctly with dashboard analytics data renders learn xmlns="http://www.w3.org/2000/svg" > @@ -6316,7 +6316,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -6367,7 +6367,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -6437,7 +6437,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -6488,7 +6488,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -6625,7 +6625,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -6727,7 +6727,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -6778,7 +6778,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -6891,7 +6891,7 @@ exports[` renders correctly with dashboard analytics data renders regis xmlns="http://www.w3.org/2000/svg" > @@ -7036,7 +7036,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7087,7 +7087,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7157,7 +7157,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7208,7 +7208,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7345,7 +7345,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7447,7 +7447,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7498,7 +7498,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7611,7 +7611,7 @@ exports[` renders correctly with dashboard analytics data renders top a xmlns="http://www.w3.org/2000/svg" > @@ -7743,7 +7743,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -7765,7 +7765,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -7809,7 +7809,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -7860,7 +7860,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -7930,7 +7930,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -7981,7 +7981,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -8118,7 +8118,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -8220,7 +8220,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -8271,7 +8271,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -8486,7 +8486,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -8574,7 +8574,7 @@ exports[` renders correctly with dashboard insights data renders dashbo xmlns="http://www.w3.org/2000/svg" > @@ -8665,7 +8665,7 @@ exports[` renders correctly with error state 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9045,7 +9045,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9096,7 +9096,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9166,7 +9166,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9217,7 +9217,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9354,7 +9354,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9456,7 +9456,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9507,7 +9507,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9722,7 +9722,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -9810,7 +9810,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/App/index.jsx b/src/components/App/index.jsx index 861bd7d056..5915c030a4 100644 --- a/src/components/App/index.jsx +++ b/src/components/App/index.jsx @@ -1,6 +1,11 @@ import React, { useEffect, useMemo } from 'react'; import { Switch, Route, Redirect } from 'react-router-dom'; import { Helmet } from 'react-helmet'; +import { + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { initializeHotjar } from '@edx/frontend-enterprise-hotjar'; import { AuthenticatedPageRoute, PageRoute, AppProvider } from '@edx/frontend-platform/react'; import { logError } from '@edx/frontend-platform/logging'; @@ -19,6 +24,19 @@ import { SystemWideWarningBanner } from '../system-wide-banner'; import store from '../../data/store'; import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Specifying a longer `staleTime` of 20 seconds means queries will not refetch their data + // as often; mitigates making duplicate queries when within the `staleTime` window, instead + // relying on the cached data until the `staleTime` window has exceeded. This may be modified + // per-query, as needed, if certain queries expect to be more up-to-date than others. Allows + // `useQuery` to be used as a state manager. + staleTime: 1000 * 20, + }, + }, +}); + const AppWrapper = () => { const apiClient = getAuthenticatedHttpClient(); const config = getConfig(); @@ -55,64 +73,67 @@ const AppWrapper = () => { }, [config]); return ( - - - {isMaintenanceAlertOpen && ( - - {config.MAINTENANCE_ALERT_MESSAGE} - - )} -
- - } - authenticatedAPIClient={apiClient} - redirect={`${process.env.BASE_URL}/enterprises`} - /> - - - ( - - - - - )} - /> - } - authenticatedAPIClient={apiClient} - redirect={process.env.BASE_URL} + + + + - - -
- + {isMaintenanceAlertOpen && ( + + {config.MAINTENANCE_ALERT_MESSAGE} + + )} +
+ + } + authenticatedAPIClient={apiClient} + redirect={`${process.env.BASE_URL}/enterprises`} + /> + + + ( + + + + + )} + /> + } + authenticatedAPIClient={apiClient} + redirect={process.env.BASE_URL} + /> + + +
+ + ); }; diff --git a/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap b/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap index 13de3d3b8a..b66b03dfe0 100644 --- a/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap +++ b/src/components/CodeManagement/tests/__snapshots__/ManageCodesTab.test.jsx.snap @@ -67,7 +67,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -105,7 +105,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -180,12 +180,9 @@ Array [ xmlns="http://www.w3.org/2000/svg" > - @@ -271,7 +268,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -309,7 +306,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -384,7 +381,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -480,7 +477,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -518,7 +515,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -657,7 +654,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -695,7 +692,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -862,7 +859,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -993,7 +990,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -1058,7 +1055,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > @@ -1094,7 +1091,7 @@ Array [ xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap b/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap index eb08be87a9..61f894cf87 100644 --- a/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap +++ b/src/components/CodeSearchResults/__snapshots__/CodeSearchResults.test.jsx.snap @@ -59,7 +59,7 @@ exports[` basic rendering should render empty table data 1` xmlns="http://www.w3.org/2000/svg" > @@ -86,7 +86,7 @@ exports[` basic rendering should render empty table data 1` xmlns="http://www.w3.org/2000/svg" > @@ -165,7 +165,7 @@ exports[` basic rendering should render error 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -192,7 +192,7 @@ exports[` basic rendering should render error 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -279,7 +279,7 @@ exports[` basic rendering should render loading 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -650,7 +650,7 @@ exports[` basic rendering should render table data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -754,7 +754,7 @@ exports[` basic rendering should render table data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -857,7 +857,7 @@ exports[` basic rendering should render table data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -979,7 +979,7 @@ exports[` basic rendering should render table data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -1015,7 +1015,7 @@ exports[` basic rendering should render table data 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -1092,7 +1092,7 @@ exports[` basic rendering should render table data when sea xmlns="http://www.w3.org/2000/svg" > @@ -1283,7 +1283,7 @@ exports[` basic rendering should render table data when sea xmlns="http://www.w3.org/2000/svg" > @@ -1319,7 +1319,7 @@ exports[` basic rendering should render table data when sea xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap b/src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap index 41fbb7a365..722c7b349f 100644 --- a/src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap +++ b/src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap @@ -19,7 +19,7 @@ exports[`CompletedLearnersTable renders empty state correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/Coupon/__snapshots__/Coupon.test.jsx.snap b/src/components/Coupon/__snapshots__/Coupon.test.jsx.snap index 434813392e..a77defc1e3 100644 --- a/src/components/Coupon/__snapshots__/Coupon.test.jsx.snap +++ b/src/components/Coupon/__snapshots__/Coupon.test.jsx.snap @@ -111,7 +111,7 @@ exports[` renders correctly with error state 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -135,7 +135,7 @@ exports[` renders correctly with error state 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -269,7 +269,7 @@ exports[` renders correctly with max uses 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -398,7 +398,7 @@ exports[` renders correctly without max uses 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap b/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap index ea62dd2bae..415407b4ae 100644 --- a/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap +++ b/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap @@ -19,7 +19,7 @@ exports[`EnrolledLearnersForInactiveCoursesTable renders empty state correctly 1 xmlns="http://www.w3.org/2000/svg" > @@ -315,7 +315,7 @@ exports[`EnrolledLearnersForInactiveCoursesTable renders enrolled learners for i xmlns="http://www.w3.org/2000/svg" > @@ -351,7 +351,7 @@ exports[`EnrolledLearnersForInactiveCoursesTable renders enrolled learners for i xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap b/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap index 301e26406f..829f004573 100644 --- a/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap +++ b/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap @@ -19,7 +19,7 @@ exports[`EnrolledLearnersTable renders empty state correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/EnrollmentsTable/__snapshots__/EnrollmentsTable.test.jsx.snap b/src/components/EnrollmentsTable/__snapshots__/EnrollmentsTable.test.jsx.snap index 589f3003a1..5f9d166431 100644 --- a/src/components/EnrollmentsTable/__snapshots__/EnrollmentsTable.test.jsx.snap +++ b/src/components/EnrollmentsTable/__snapshots__/EnrollmentsTable.test.jsx.snap @@ -19,7 +19,7 @@ exports[`EnrollmentsTable renders empty state correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/ErrorPage/__snapshots__/ErrorPage.test.jsx.snap b/src/components/ErrorPage/__snapshots__/ErrorPage.test.jsx.snap index 46e31f69b3..7572adf922 100644 --- a/src/components/ErrorPage/__snapshots__/ErrorPage.test.jsx.snap +++ b/src/components/ErrorPage/__snapshots__/ErrorPage.test.jsx.snap @@ -31,7 +31,7 @@ exports[` renders correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -84,18 +84,14 @@ exports[` renders correctly for 403 errors 1`] = `

For assistance, please contact the edX Customer Success team at - - - customersuccess@edx.org - - + customersuccess@edx.org + .

diff --git a/src/components/ForbiddenPage/__snapshots__/ForbiddenPage.test.jsx.snap b/src/components/ForbiddenPage/__snapshots__/ForbiddenPage.test.jsx.snap index f0602540eb..de29dfe42b 100644 --- a/src/components/ForbiddenPage/__snapshots__/ForbiddenPage.test.jsx.snap +++ b/src/components/ForbiddenPage/__snapshots__/ForbiddenPage.test.jsx.snap @@ -21,18 +21,14 @@ exports[` renders correctly 1`] = `

For assistance, please contact the edX Customer Success team at - - - customersuccess@edx.org - - + customersuccess@edx.org + .

diff --git a/src/components/LearnerActivityTable/__snapshots__/LearnerActivityTable.test.jsx.snap b/src/components/LearnerActivityTable/__snapshots__/LearnerActivityTable.test.jsx.snap index 37df5114a7..045d0d6f0e 100644 --- a/src/components/LearnerActivityTable/__snapshots__/LearnerActivityTable.test.jsx.snap +++ b/src/components/LearnerActivityTable/__snapshots__/LearnerActivityTable.test.jsx.snap @@ -437,7 +437,7 @@ exports[`LearnerActivityTable renders active learners table correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -473,7 +473,7 @@ exports[`LearnerActivityTable renders active learners table correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -507,7 +507,7 @@ exports[`LearnerActivityTable renders empty state correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -923,7 +923,7 @@ exports[`LearnerActivityTable renders inactive past month learners table correct xmlns="http://www.w3.org/2000/svg" > @@ -959,7 +959,7 @@ exports[`LearnerActivityTable renders inactive past month learners table correct xmlns="http://www.w3.org/2000/svg" > @@ -1373,7 +1373,7 @@ exports[`LearnerActivityTable renders inactive past week learners table correctl xmlns="http://www.w3.org/2000/svg" > @@ -1409,7 +1409,7 @@ exports[`LearnerActivityTable renders inactive past week learners table correctl xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap b/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap index 415a2c1b3e..7586dfc531 100644 --- a/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap +++ b/src/components/NumberCard/__snapshots__/NumberCard.test.jsx.snap @@ -30,7 +30,7 @@ exports[` renders correctly with detail actions 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -81,7 +81,7 @@ exports[` renders correctly with detail actions 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -166,7 +166,7 @@ exports[` renders correctly without detail actions 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap b/src/components/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap index 5bd0b51726..1b48824ca2 100644 --- a/src/components/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap +++ b/src/components/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap @@ -209,7 +209,7 @@ exports[`PastWeekPassedLearnersTable renders table correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -245,7 +245,7 @@ exports[`PastWeekPassedLearnersTable renders table correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/ReduxFormCheckbox/__snapshots__/ReduxFormCheckbox.test.jsx.snap b/src/components/ReduxFormCheckbox/__snapshots__/ReduxFormCheckbox.test.jsx.snap index 89d00e3afa..e3c7a71ec3 100644 --- a/src/components/ReduxFormCheckbox/__snapshots__/ReduxFormCheckbox.test.jsx.snap +++ b/src/components/ReduxFormCheckbox/__snapshots__/ReduxFormCheckbox.test.jsx.snap @@ -11,6 +11,7 @@ exports[` renders checked correctly 1`] = ` checked={true} className="pgn__form-checkbox-input" defaultValue={false} + disabled={false} id="id" type="checkbox" /> @@ -35,6 +36,7 @@ exports[` renders unchecked correctly 1`] = ` checked={false} className="pgn__form-checkbox-input" defaultValue={false} + disabled={false} id="id" type="checkbox" /> diff --git a/src/components/RegisteredLearnersTable/__snapshots__/RegisteredLearnersTable.test.jsx.snap b/src/components/RegisteredLearnersTable/__snapshots__/RegisteredLearnersTable.test.jsx.snap index c66304e864..a371442a53 100644 --- a/src/components/RegisteredLearnersTable/__snapshots__/RegisteredLearnersTable.test.jsx.snap +++ b/src/components/RegisteredLearnersTable/__snapshots__/RegisteredLearnersTable.test.jsx.snap @@ -19,7 +19,7 @@ exports[`RegisteredLearnersTable renders empty state correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/SearchBar/__snapshots__/SearchBar.test.jsx.snap b/src/components/SearchBar/__snapshots__/SearchBar.test.jsx.snap index 493225f51d..d93f96f39b 100644 --- a/src/components/SearchBar/__snapshots__/SearchBar.test.jsx.snap +++ b/src/components/SearchBar/__snapshots__/SearchBar.test.jsx.snap @@ -51,7 +51,7 @@ exports[` renders correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 5a78bf11e8..a6cfd936d1 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -70,25 +70,25 @@ const Sidebar = ({ { title: 'Learner Progress Report', to: `${baseUrl}/admin/${ROUTE_NAMES.learners}`, - icon: , + icon: , }, { title: 'Analytics', to: `${baseUrl}/admin/${ROUTE_NAMES.analytics}`, - icon: , + icon: , hidden: !features.ANALYTICS || !enableAnalyticsScreen, }, { title: 'Code Management', to: `${baseUrl}/admin/${ROUTE_NAMES.codeManagement}`, - icon: , + icon: , hidden: !features.CODE_MANAGEMENT || !enableCodeManagementScreen, notification: !!subsidyRequestsCounts.couponCodes, }, { title: 'Subscription Management', to: `${baseUrl}/admin/${ROUTE_NAMES.subscriptionManagement}`, - icon: , + icon: , hidden: !enableSubscriptionManagementScreen, notification: !!subsidyRequestsCounts.subscriptionLicenses, }, @@ -96,33 +96,33 @@ const Sidebar = ({ title: 'Learner Credit Management', id: TOUR_TARGETS.LEARNER_CREDIT, to: `${baseUrl}/admin/${ROUTE_NAMES.learnerCredit}`, - icon: , + icon: , hidden: !canManageLearnerCredit, }, { title: 'Highlights', id: TOUR_TARGETS.CONTENT_HIGHLIGHTS, to: `${baseUrl}/admin/${ROUTE_NAMES.contentHighlights}`, - icon: , + icon: , hidden: !FEATURE_CONTENT_HIGHLIGHTS || !enterpriseCuration?.isHighlightFeatureActive, }, { title: 'Reporting Configurations', to: `${baseUrl}/admin/${ROUTE_NAMES.reporting}`, - icon: , + icon: , hidden: !features.REPORTING_CONFIGURATIONS || !enableReportingConfigScreen, }, { title: 'Settings', id: TOUR_TARGETS.SETTINGS_SIDEBAR, to: `${baseUrl}/admin/${ROUTE_NAMES.settings}`, - icon: , + icon: , }, // NOTE: keep "Support" link the last nav item { title: 'Support', to: configuration.ENTERPRISE_SUPPORT_URL, - icon: , + icon: , hidden: !features.SUPPORT, external: true, }, diff --git a/src/components/SubsidyRequestManagementTable/tests/__snapshots__/SubsidyRequestManagementTable.test.jsx.snap b/src/components/SubsidyRequestManagementTable/tests/__snapshots__/SubsidyRequestManagementTable.test.jsx.snap index 110de1903e..888fba9fb4 100644 --- a/src/components/SubsidyRequestManagementTable/tests/__snapshots__/SubsidyRequestManagementTable.test.jsx.snap +++ b/src/components/SubsidyRequestManagementTable/tests/__snapshots__/SubsidyRequestManagementTable.test.jsx.snap @@ -417,7 +417,7 @@ exports[`SubsidyRequestManagementTable renders data in a table as expected 1`] = xmlns="http://www.w3.org/2000/svg" > @@ -452,7 +452,7 @@ exports[`SubsidyRequestManagementTable renders data in a table as expected 1`] = xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx new file mode 100644 index 0000000000..92826cb93f --- /dev/null +++ b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Hyperlink } from '@edx/paragon'; + +import { configuration } from '../../config'; +import EmailAddressTableCell from './EmailAddressTableCell'; + +const AssignmentDetailsTableCell = ({ row, enterpriseSlug }) => { + const { ENTERPRISE_LEARNER_PORTAL_URL } = configuration; + return ( + <> + +
+ + View course + +
+ + ); +}; + +const mapStateToProps = state => ({ + enterpriseSlug: state.portalConfiguration.enterpriseSlug, +}); + +AssignmentDetailsTableCell.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + uuid: PropTypes.string.isRequired, + learnerEmail: PropTypes.string.isRequired, + contentKey: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + enterpriseSlug: PropTypes.string, +}; + +export default connect(mapStateToProps)(AssignmentDetailsTableCell); diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx new file mode 100644 index 0000000000..5b2b94b5b0 --- /dev/null +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DataTable } from '@edx/paragon'; + +import TableTextFilter from './TableTextFilter'; +import CustomDataTableEmptyState from './CustomDataTableEmptyState'; +import AssignmentDetailsTableCell from './AssignmentDetailsTableCell'; +import { DEFAULT_PAGE, PAGE_SIZE, formatPrice } from './data'; + +const FilterStatus = (rest) => ; + +const BudgetAssignmentsTable = ({ + isLoading, + tableData, + fetchTableData, +}) => ( + `-${formatPrice(row.original.contentQuantity / 100, { maximumFractionDigits: 0 })}`, + disableFilters: true, + }, + ]} + initialTableOptions={{ + getRowId: row => row?.uuid?.toString(), + }} + initialState={{ + pageSize: PAGE_SIZE, + pageIndex: DEFAULT_PAGE, + sortBy: [], + filters: [], + }} + fetchData={fetchTableData} + data={tableData.results} + itemCount={tableData.count} + pageCount={tableData.numPages} + EmptyTableComponent={CustomDataTableEmptyState} + /> +); + +BudgetAssignmentsTable.propTypes = { + isLoading: PropTypes.bool.isRequired, + tableData: PropTypes.shape().isRequired, + fetchTableData: PropTypes.func.isRequired, +}; + +export default BudgetAssignmentsTable; diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index a593012a63..28a152fce7 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -1,47 +1,70 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { useParams } from 'react-router-dom'; +import { Stack } from '@edx/paragon'; -import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; -import { useOfferRedemptions, isUUID } from './data'; +import BudgetDetailRedemptions from './BudgetDetailRedemptions'; +import BudgetDetailAssignments from './BudgetDetailAssignments'; +import { + useOfferRedemptions, + useBudgetContentAssignments, + useBudgetId, + useSubsidyAccessPolicy, +} from './data'; const BudgetDetailActivityTabContents = ({ enterpriseUUID, - enterpriseSlug, - enableLearnerPortal, + enterpriseFeatures, }) => { - const { budgetId } = useParams(); - const enterpriseOfferId = isUUID(budgetId) ? null : budgetId; - const subsidyAccessPolicyId = isUUID(budgetId) ? budgetId : null; + const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId(); + const { + data: subsidyAccessPolicy, + } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + const isTopDownAssignmentEnabled = enterpriseFeatures?.topDownAssignmentRealTimeLcm; + const { isLoading: isLoadingOfferRedemptions, offerRedemptions, fetchOfferRedemptions, } = useOfferRedemptions(enterpriseUUID, enterpriseOfferId, subsidyAccessPolicyId); + const { + isLoading: isLoadingContentAssignments, + contentAssignments, + fetchContentAssignments, + } = useBudgetContentAssignments({ + assignmentConfigurationUUID: subsidyAccessPolicy?.assignmentConfiguration?.uuid, + isEnabled: subsidyAccessPolicy?.isAssignable && isTopDownAssignmentEnabled, + }); + return ( - + + + + ); }; const mapStateToProps = state => ({ enterpriseUUID: state.portalConfiguration.enterpriseId, - enterpriseSlug: state.portalConfiguration.enterpriseSlug, - enableLearnerPortal: state.portalConfiguration.enableLearnerPortal, + enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, }); BudgetDetailActivityTabContents.propTypes = { enterpriseUUID: PropTypes.string.isRequired, - enterpriseSlug: PropTypes.string.isRequired, - enableLearnerPortal: PropTypes.bool.isRequired, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool, + }), }; export default connect(mapStateToProps)(BudgetDetailActivityTabContents); diff --git a/src/components/learner-credit-management/BudgetDetailAssignments.jsx b/src/components/learner-credit-management/BudgetDetailAssignments.jsx new file mode 100644 index 0000000000..f4ba6eb8f2 --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailAssignments.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import BudgetAssignmentsTable from './BudgetAssignmentsTable'; + +const BudgetDetailAssignments = ({ + isEnabled, + isLoading, + tableData, + fetchTableData, +}) => { + if (!isEnabled) { + return null; + } + + return ( +
+

Assigned

+

+ Assigned activity earmarks funds in your budget so you can't overspend. For funds to move + from assigned to spent, your learners must complete enrollment. +

+ +
+ ); +}; + +BudgetDetailAssignments.propTypes = { + isEnabled: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, + tableData: PropTypes.shape().isRequired, + fetchTableData: PropTypes.func.isRequired, +}; + +export default BudgetDetailAssignments; diff --git a/src/components/learner-credit-management/BudgetDetailPage.jsx b/src/components/learner-credit-management/BudgetDetailPage.jsx index 29b83b4058..1ad1ff56cc 100644 --- a/src/components/learner-credit-management/BudgetDetailPage.jsx +++ b/src/components/learner-credit-management/BudgetDetailPage.jsx @@ -1,61 +1,38 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; -import { - Row, - Col, - Breadcrumb, - Container, -} from '@edx/paragon'; -import { connect } from 'react-redux'; -import { Helmet } from 'react-helmet'; -import { Link } from 'react-router-dom'; -import Hero from '../Hero'; +import React from 'react'; +import { Skeleton, Stack } from '@edx/paragon'; -import LoadingMessage from '../LoadingMessage'; -import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; -import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; import BudgetDetailTabsAndRoutes from './BudgetDetailTabsAndRoutes'; +import BudgetDetailPageWrapper from './BudgetDetailPageWrapper'; +import BudgetDetailPageHeader from './BudgetDetailPageHeader'; -const PAGE_TITLE = 'Learner Credit Management'; +const BudgetDetailPage = () => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { + isInitialLoading: isInitialLoadingSubsidyAccessPolicy, + data: subsidyAccessPolicy, + } = useSubsidyAccessPolicy(subsidyAccessPolicyId); -const BudgetDetailPage = ({ enterpriseSlug }) => { - const { isLoading } = useContext(EnterpriseSubsidiesContext); - if (isLoading) { - return ; + if (isInitialLoadingSubsidyAccessPolicy) { + return ( + + + + + + loading budget details + + ); } - const links = [ - { - label: 'Budgets', - to: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}`, - }, - ]; + return ( - <> - - - - - - - - + + + - - + + ); }; -const mapStateToProps = state => ({ - enterpriseSlug: state.portalConfiguration.enterpriseSlug, -}); - -BudgetDetailPage.propTypes = { - enterpriseSlug: PropTypes.string.isRequired, -}; - -export default connect(mapStateToProps)(BudgetDetailPage); +export default BudgetDetailPage; diff --git a/src/components/learner-credit-management/BudgetDetailPageHeader.jsx b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx new file mode 100644 index 0000000000..ad84c6724e --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { + Row, Col, Breadcrumb, Stack, +} from '@edx/paragon'; + +import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; + +const BudgetDetailPageHeader = ({ enterpriseSlug }) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const budgetDisplayName = subsidyAccessPolicy?.displayName || 'Overview'; + return ( + + + + + + + {budgetDisplayName && ( + + +

{budgetDisplayName}

+ +
+ )} +
+ ); +}; + +const mapStateToProps = state => ({ + enterpriseSlug: state.portalConfiguration.enterpriseSlug, +}); + +BudgetDetailPageHeader.propTypes = { + enterpriseSlug: PropTypes.string.isRequired, +}; + +export default connect(mapStateToProps)(BudgetDetailPageHeader); diff --git a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx new file mode 100644 index 0000000000..1651094dd4 --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Helmet } from 'react-helmet'; +import { Container } from '@edx/paragon'; + +import Hero from '../Hero'; + +const PAGE_TITLE = 'Learner Credit Management'; + +const BudgetDetailPageWrapper = ({ subsidyAccessPolicy, children }) => { + // display name is an optional field, and may not be set for all budgets so fallback to "Overview" + // similar to the display name logic for budgets on the overview page route. + const budgetDisplayName = subsidyAccessPolicy?.displayName || 'Overview'; + const helmetPageTitle = budgetDisplayName ? `${budgetDisplayName} - ${PAGE_TITLE}` : PAGE_TITLE; + return ( + <> + + + + {children} + + + ); +}; + +BudgetDetailPageWrapper.propTypes = { + children: PropTypes.node.isRequired, + subsidyAccessPolicy: PropTypes.shape(), +}; + +export default BudgetDetailPageWrapper; diff --git a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx new file mode 100644 index 0000000000..bccf80aa6c --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; + +const BudgetDetailRedemptions = ({ + isLoading, + offerRedemptions, + fetchOfferRedemptions, + enterpriseUUID, + enterpriseSlug, + enableLearnerPortal, +}) => ( +
+

Spent

+

+ Spent activity is driven by completed enrollments. Enrollment data is automatically updated every 12 hours. + Come back later to view more recent enrollments. +

+ +
+); + +const mapStateToProps = state => ({ + enterpriseUUID: state.portalConfiguration.enterpriseId, + enterpriseSlug: state.portalConfiguration.enterpriseSlug, + enableLearnerPortal: state.portalConfiguration.enableLearnerPortal, +}); + +BudgetDetailRedemptions.propTypes = { + enterpriseUUID: PropTypes.string.isRequired, + enterpriseSlug: PropTypes.string.isRequired, + enableLearnerPortal: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, + offerRedemptions: PropTypes.shape({ + results: PropTypes.arrayOf(PropTypes.shape({ + userEmail: PropTypes.string, + courseTitle: PropTypes.string.isRequired, + courseListPrice: PropTypes.number.isRequired, + enrollmentDate: PropTypes.string.isRequired, + courseProductLine: PropTypes.string.isRequired, + })), + itemCount: PropTypes.number.isRequired, + pageCount: PropTypes.number.isRequired, + }).isRequired, + fetchOfferRedemptions: PropTypes.func.isRequired, +}; + +export default connect(mapStateToProps)(BudgetDetailRedemptions); diff --git a/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx b/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx index 9932cd4cdc..1d0ef0db7f 100644 --- a/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx +++ b/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx @@ -9,7 +9,7 @@ import { BUDGET_DETAIL_ACTIVITY_TAB, BUDGET_DETAIL_CATALOG_TAB, } from './data/constants'; -import { useBudgetDetailTabs } from './data'; +import { useBudgetDetailTabs, useBudgetId, useSubsidyAccessPolicy } from './data'; import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; import NotFoundPage from '../NotFoundPage'; import EVENT_NAMES from '../../eventTracking'; @@ -18,17 +18,18 @@ import BudgetDetailCatalogTabContents from './BudgetDetailCatalogTabContents'; const DEFAULT_TAB = BUDGET_DETAIL_ACTIVITY_TAB; -function isSupportedTabKey({ tabKey, enterpriseFeatures }) { +function isSupportedTabKey({ tabKey, isBudgetAssignable, enterpriseFeatures }) { const supportedTabs = [BUDGET_DETAIL_ACTIVITY_TAB]; - if (enterpriseFeatures.topDownAssignmentRealTimeLcm) { + if (enterpriseFeatures.topDownAssignmentRealTimeLcm && isBudgetAssignable) { supportedTabs.push(BUDGET_DETAIL_CATALOG_TAB); } return supportedTabs.includes(tabKey); } -function getInitialTabKey(routeActiveTabKey, { enterpriseFeatures }) { +function getInitialTabKey(routeActiveTabKey, { isBudgetAssignable, enterpriseFeatures }) { const isValidTabKey = isSupportedTabKey({ tabKey: routeActiveTabKey, + isBudgetAssignable, enterpriseFeatures, }); if (!isValidTabKey) { @@ -42,16 +43,27 @@ const BudgetDetailTabsAndRoutes = ({ enterpriseSlug, enterpriseFeatures, }) => { + const { activeTabKey: routeActiveTabKey } = useParams(); + const { budgetId, subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const isBudgetAssignable = !!subsidyAccessPolicy?.isAssignable; + const history = useHistory(); - const { budgetId, activeTabKey: routeActiveTabKey } = useParams(); const [activeTabKey, setActiveTabKey] = useState(getInitialTabKey( routeActiveTabKey, - { enterpriseFeatures }, + { enterpriseFeatures, isBudgetAssignable }, )); + /** + * Ensure the active tab in the UI reflects the active tab in the URL. + */ useEffect(() => { - setActiveTabKey(getInitialTabKey(routeActiveTabKey, { enterpriseFeatures })); - }, [routeActiveTabKey, enterpriseFeatures]); + const initialTabKey = getInitialTabKey( + routeActiveTabKey, + { enterpriseFeatures, isBudgetAssignable }, + ); + setActiveTabKey(initialTabKey); + }, [routeActiveTabKey, enterpriseFeatures, isBudgetAssignable]); const handleTabSelect = (nextActiveTabKey) => { setActiveTabKey(nextActiveTabKey); @@ -66,6 +78,7 @@ const BudgetDetailTabsAndRoutes = ({ const tabs = useBudgetDetailTabs({ activeTabKey, + isBudgetAssignable, enterpriseFeatures, ActivityTabElement: BudgetDetailActivityTabContents, CatalogTabElement: BudgetDetailCatalogTabContents, @@ -73,6 +86,7 @@ const BudgetDetailTabsAndRoutes = ({ if (!isSupportedTabKey({ tabKey: routeActiveTabKey || activeTabKey, + isBudgetAssignable, enterpriseFeatures, })) { return ; diff --git a/src/components/learner-credit-management/SpendTableEmptyState.jsx b/src/components/learner-credit-management/CustomDataTableEmptyState.jsx similarity index 75% rename from src/components/learner-credit-management/SpendTableEmptyState.jsx rename to src/components/learner-credit-management/CustomDataTableEmptyState.jsx index 20f5c9165a..962f68f383 100644 --- a/src/components/learner-credit-management/SpendTableEmptyState.jsx +++ b/src/components/learner-credit-management/CustomDataTableEmptyState.jsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import { DataTable, DataTableContext } from '@edx/paragon'; -const SpendTableEmptyState = () => { +const CustomDataTableEmptyState = () => { const { isLoading } = useContext(DataTableContext); if (isLoading) { return null; @@ -9,4 +9,4 @@ const SpendTableEmptyState = () => { return ; }; -export default SpendTableEmptyState; +export default CustomDataTableEmptyState; diff --git a/src/components/learner-credit-management/EmailAddressTableCell.jsx b/src/components/learner-credit-management/EmailAddressTableCell.jsx index a85d1094c8..d635932422 100644 --- a/src/components/learner-credit-management/EmailAddressTableCell.jsx +++ b/src/components/learner-credit-management/EmailAddressTableCell.jsx @@ -7,15 +7,51 @@ import { import { InfoOutline } from '@edx/paragon/icons'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; -const EmailAddressTableCell = ({ row, enterpriseUUID }) => { - if (row.original.userEmail) { - return {row.original.userEmail}; +/** + * Conditionally renders either the email address, when available, or a "Email hidden" message, + * including a popover that includes additional messaging about why the email is hidden. + * + * In the case an email is hidden, includes Segment events for when the popover is opened and closed. Note + * the row identifier passed as event metadata may be different depending on which table this component is + * used in and what data source is backing it, i.e.: + * - "Spent" table backed by analytics API uses the `enterpriseEnrollmentId` + * - "Spent" table backed by transactions API uses the `fulfillmentUUID`. + * - "Assigned" table uses the `ContentAssignment` UUID + * + * @param {string} tableId Unique identifier for the table this component is used in. Used to namespace + * the Segment events. + * @param {string} userEmail The email address to render, if available. + * @param {number} [enterpriseEnrollmentId] The enterprise enrollment ID, used by "Spent" table backed by analytics API. + * @param {string} [fulfillmentIdentifier] The UUID of a `LearnerCreditEnterpriseCourseEnrollment`, used by "Spent" + * table backed by transactions API. + * @param {string} [contentAssignmentUUID] The UUID of a content assignment, used by "Assigned" table. + * @param {string} enterpriseUUID The UUID of the enterprise, used for Segment events. + * + * @returns A React component that renders either an email address or a "Email hidden" message. + */ +const EmailAddressTableCell = ({ + tableId, + userEmail, + enterpriseEnrollmentId, + contentAssignmentUUID, + fulfillmentIdentifier, + enterpriseUUID, +}) => { + if (userEmail) { + return ( + + {userEmail} + + ); } return ( - - Email hidden + + Email hidden @@ -28,19 +64,15 @@ const EmailAddressTableCell = ({ row, enterpriseUUID }) => { onEntered={() => { sendEnterpriseTrackEvent( enterpriseUUID, - 'edx.ui.enterprise.admin_portal.learner-credit-management.table.email-hidden-popover.opened', - { - enterpriseEnrollmentId: row.original.enterpriseEnrollmentId, - }, + `edx.ui.enterprise.admin_portal.learner-credit-management.${tableId}.email-hidden-popover.opened`, + { enterpriseEnrollmentId, fulfillmentIdentifier, contentAssignmentUUID }, ); }} onExited={() => { sendEnterpriseTrackEvent( enterpriseUUID, - 'edx.ui.enterprise.admin_portal.learner-credit-management.table.email-hidden-popover.dismissed', - { - enterpriseEnrollmentId: row.original.enterpriseEnrollmentId, - }, + `edx.ui.enterprise.admin_portal.learner-credit-management.${tableId}.email-hidden-popover.dismissed`, + { enterpriseEnrollmentId, fulfillmentIdentifier, contentAssignmentUUID }, ); }} > @@ -57,12 +89,11 @@ const EmailAddressTableCell = ({ row, enterpriseUUID }) => { }; EmailAddressTableCell.propTypes = { - row: PropTypes.shape({ - original: PropTypes.shape({ - userEmail: PropTypes.string, - enterpriseEnrollmentId: PropTypes.number, - }), - }).isRequired, + tableId: PropTypes.string.isRequired, + userEmail: PropTypes.string, // may be undefined/null if email is hidden + enterpriseEnrollmentId: PropTypes.number, + contentAssignmentUUID: PropTypes.string, + fulfillmentIdentifier: PropTypes.string, enterpriseUUID: PropTypes.string.isRequired, }; diff --git a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx index b74e51cd25..e5f1309490 100644 --- a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx +++ b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx @@ -2,13 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; import { DataTable } from '@edx/paragon'; + import TableTextFilter from './TableTextFilter'; -import SpendTableEmptyState from './SpendTableEmptyState'; +import CustomDataTableEmptyState from './CustomDataTableEmptyState'; import SpendTableEnrollmentDetails from './SpendTableEnrollmentDetails'; import { getCourseProductLineText } from '../../utils'; - -export const PAGE_SIZE = 20; -export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array +import { PAGE_SIZE, DEFAULT_PAGE } from './data'; const FilterStatus = (rest) => ; @@ -16,72 +15,61 @@ const LearnerCreditAllocationTable = ({ isLoading, tableData, fetchTableData, -}) => { - const defaultFilter = []; - return ( - <> -

Spent

-

- Spent activity is driven by completed enrollments. Enrollment data is automatically updated every 12 hours. - Come back later to view more recent enrollments. -

- dayjs(row.values.enrollmentDate).format('MMM D, YYYY'), - disableFilters: true, - }, - { - Header: 'Enrollment details', - accessor: 'enrollmentDetails', - Cell: SpendTableEnrollmentDetails, - disableFilters: false, - disableSortBy: true, - }, - { - Header: 'Amount', - accessor: 'courseListPrice', - Cell: ({ row }) => `$${row.values.courseListPrice}`, - disableFilters: true, - }, - { - Header: 'Product', - accessor: 'courseProductLine', - Cell: ({ row }) => getCourseProductLineText(row.values.courseProductLine), - disableFilters: true, - }, - ]} - initialTableOptions={{ - getRowId: row => row?.uuid?.toString(), - }} - initialState={{ - pageSize: PAGE_SIZE, - pageIndex: DEFAULT_PAGE, - sortBy: [ - { id: 'enrollmentDate', desc: true }, - ], - filters: defaultFilter, - }} - fetchData={fetchTableData} - data={tableData.results} - itemCount={tableData.itemCount} - pageCount={tableData.pageCount} - EmptyTableComponent={SpendTableEmptyState} - /> - - ); -}; +}) => ( + dayjs(row.values.enrollmentDate).format('MMM D, YYYY'), + disableFilters: true, + }, + { + Header: 'Enrollment details', + accessor: 'enrollmentDetails', + Cell: SpendTableEnrollmentDetails, + disableSortBy: true, + }, + { + Header: 'Amount', + accessor: 'courseListPrice', + Cell: ({ row }) => `$${row.values.courseListPrice}`, + disableFilters: true, + }, + { + Header: 'Product', + accessor: 'courseProductLine', + Cell: ({ row }) => getCourseProductLineText(row.values.courseProductLine), + disableFilters: true, + }, + ]} + initialTableOptions={{ + getRowId: row => row?.uuid?.toString(), + }} + initialState={{ + pageSize: PAGE_SIZE, + pageIndex: DEFAULT_PAGE, + sortBy: [ + { id: 'enrollmentDate', desc: true }, + ], + filters: [], + }} + fetchData={fetchTableData} + data={tableData.results} + itemCount={tableData.itemCount} + pageCount={tableData.pageCount} + EmptyTableComponent={CustomDataTableEmptyState} + /> +); LearnerCreditAllocationTable.propTypes = { isLoading: PropTypes.bool.isRequired, diff --git a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx index 227f917290..dafd44b88f 100644 --- a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx +++ b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx @@ -12,17 +12,24 @@ const SpendTableEnrollmentDetailsContents = ({ enterpriseSlug, }) => ( <> - +
{enableLearnerPortal ? ( {row.original.courseTitle} ) : ( - {row.original.courseTitle} + {row.original.courseTitle} )}
@@ -32,6 +39,9 @@ const rowPropType = PropTypes.shape({ original: PropTypes.shape({ courseKey: PropTypes.string.isRequired, courseTitle: PropTypes.string.isRequired, + userEmail: PropTypes.string.isRequired, + enterpriseEnrollmentId: PropTypes.number, + fulfillmentIdentifier: PropTypes.string, }).isRequired, }).isRequired; diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 37f453bd4a..8db03bb0ce 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -32,5 +32,8 @@ export const LANGUAGE_REFINEMENT = 'language'; // Learning types export const CONTENT_TYPE_COURSE = 'course'; export const EXEC_ED_TITLE = 'Executive Education'; - export const EXEC_COURSE_TYPE = 'executive-education-2u'; + +// Number of items to display per page in Budget Detail assignment/spend tables +export const PAGE_SIZE = 25; +export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array diff --git a/src/components/learner-credit-management/data/hooks/hooks.js b/src/components/learner-credit-management/data/hooks/hooks.js deleted file mode 100644 index 306ad58fde..0000000000 --- a/src/components/learner-credit-management/data/hooks/hooks.js +++ /dev/null @@ -1,13 +0,0 @@ -import { useMemo, useState } from 'react'; - -import { CONTENT_TYPE_COURSE } from '../constants'; - -// eslint-disable-next-line import/prefer-default-export -export const useSelectedCourse = () => { - const [course, setCourse] = useState(null); - const isCourse = useMemo( - () => course?.contentType === CONTENT_TYPE_COURSE, - [course], - ); - return [course, setCourse, isCourse]; -}; diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index c3bebd2b82..3b21c9be3f 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -1,3 +1,6 @@ export { default as useBudgetDetailTabs } from './useBudgetDetailTabs'; export { default as useOfferSummary } from './useOfferSummary'; export { default as useOfferRedemptions } from './useOfferRedemptions'; +export { default as useBudgetContentAssignments } from './useBudgetContentAssignments'; +export { default as useBudgetId } from './useBudgetId'; +export { default as useSubsidyAccessPolicy } from './useSubsidyAccessPolicy'; diff --git a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js new file mode 100644 index 0000000000..45031d520e --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js @@ -0,0 +1,65 @@ +import { useCallback, useMemo, useState } from 'react'; +import debounce from 'lodash.debounce'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; + +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; + +const initialContentAssignmentsState = { + results: [], + count: 0, + numPages: 0, + currentPage: 1, +}; + +const applyFiltersToOptions = (filters, options) => { + if (!filters || filters.length === 0) { + return; + } + const searchQuery = filters.find(filter => filter.id === 'assignmentDetails')?.value; + if (searchQuery) { + Object.assign(options, { search: searchQuery }); + } +}; + +const useBudgetContentAssignments = ({ + assignmentConfigurationUUID, + isEnabled, +}) => { + const [isLoading, setIsLoading] = useState(true); + const [contentAssignments, setContentAssignments] = useState(initialContentAssignmentsState); + + const fetchContentAssignments = useCallback((args) => { + if (!isEnabled || !assignmentConfigurationUUID) { + setIsLoading(false); + return; + } + const getContentAssignments = async () => { + setIsLoading(true); + const options = { + page: args.pageIndex + 1, // `DataTable` uses zeo-indexed array + pageSize: args.pageSize, + }; + applyFiltersToOptions(args.filters, options); + const assignmentsResponse = await EnterpriseAccessApiService.listContentAssignments( + assignmentConfigurationUUID, + options, + ); + setContentAssignments(camelCaseObject(assignmentsResponse.data)); + setIsLoading(false); + }; + getContentAssignments(); + }, [isEnabled, assignmentConfigurationUUID]); + + const debouncedFetchContentAssigments = useMemo( + () => debounce(fetchContentAssignments, 300), + [fetchContentAssignments], + ); + + return { + isLoading, + contentAssignments, + fetchContentAssignments: debouncedFetchContentAssigments, + }; +}; + +export default useBudgetContentAssignments; diff --git a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js new file mode 100644 index 0000000000..0ede9195bd --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js @@ -0,0 +1,131 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import useBudgetContentAssignments from './useBudgetContentAssignments'; // Import the hook +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; + +describe('useBudgetContentAssignments', () => { + it('does not call fetchContentAssignments if isEnabled is false', async () => { + const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ + assignmentConfigurationUUID: '123', + isEnabled: false, + })); + const { fetchContentAssignments } = result.current; + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + mockListContentAssignments.mockResolvedValue({ + data: { + results: [], + count: 0, + numPages: 0, + currentPage: 1, + }, + }); + await fetchContentAssignments({ + pageIndex: 0, + pageSize: 10, + filters: [], + }); + + await waitForNextUpdate(); + + expect(mockListContentAssignments).not.toHaveBeenCalled(); + }); + + it('should return the correct data', async () => { + const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ + assignmentConfigurationUUID: '123', + isEnabled: true, + })); + const { fetchContentAssignments } = result.current; + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + mockListContentAssignments.mockResolvedValue({ + data: { + results: [ + { + id: 1, + name: 'test', + }, + ], + count: 1, + numPages: 1, + currentPage: 1, + }, + }); + await fetchContentAssignments({ + pageIndex: 0, + pageSize: 10, + filters: [], + }); + + await waitForNextUpdate(); + + expect(result.current.isLoading).toEqual(false); + expect(result.current.contentAssignments).toEqual({ + results: [ + { + id: 1, + name: 'test', + }, + ], + count: 1, + numPages: 1, + currentPage: 1, + }); + }); + + it.each([ + { + filters: [ + { + id: 'assignmentDetails', + value: 'test', + }, + ], + hasSearchParam: true, + }, + { + filters: [ + { + id: 'other', + value: 'test', + }, + ], + hasSearchParam: false, + }, + ])('handles assignment details filter with search query parameter (%s)', async ({ filters, hasSearchParam }) => { + const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ + assignmentConfigurationUUID: '123', + isEnabled: true, + })); + const { fetchContentAssignments } = result.current; + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + mockListContentAssignments.mockResolvedValue({ + data: { + results: [ + { + id: 1, + name: 'test', + }, + ], + count: 1, + numPages: 1, + currentPage: 1, + }, + }); + await fetchContentAssignments({ + pageIndex: 0, + pageSize: 10, + filters, + }); + + await waitForNextUpdate(); + + expect(mockListContentAssignments).toHaveBeenCalledWith( + '123', + { + page: 1, + pageSize: 10, + search: hasSearchParam ? 'test' : undefined, + }, + ); + }); +}); diff --git a/src/components/learner-credit-management/data/hooks/useBudgetDetailTabs.jsx b/src/components/learner-credit-management/data/hooks/useBudgetDetailTabs.jsx index af8186a7b1..cb33ea6384 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetDetailTabs.jsx +++ b/src/components/learner-credit-management/data/hooks/useBudgetDetailTabs.jsx @@ -11,6 +11,7 @@ const TAB_CLASS_NAME = 'pt-4.5'; export const useBudgetDetailTabs = ({ activeTabKey, + isBudgetAssignable, enterpriseFeatures, ActivityTabElement, CatalogTabElement, @@ -29,7 +30,7 @@ export const useBudgetDetailTabs = ({ )} , ); - if (enterpriseFeatures.topDownAssignmentRealTimeLcm) { + if (enterpriseFeatures.topDownAssignmentRealTimeLcm && isBudgetAssignable) { tabsArray.push( { + const { budgetId } = useParams(); + const enterpriseOfferId = isUUID(budgetId) ? null : budgetId; + const subsidyAccessPolicyId = isUUID(budgetId) ? budgetId : null; + return { + budgetId, + enterpriseOfferId, + subsidyAccessPolicyId, + }; +}; + +export default useBudgetId; diff --git a/src/components/learner-credit-management/data/tests/hooks.test.js b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.js similarity index 60% rename from src/components/learner-credit-management/data/tests/hooks.test.js rename to src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.js index 38ebaeaafd..cfe7affd2d 100644 --- a/src/components/learner-credit-management/data/tests/hooks.test.js +++ b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.js @@ -1,34 +1,12 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { camelCaseObject } from '@edx/frontend-platform/utils'; -import { - useOfferSummary, - useOfferRedemptions, -} from '../hooks'; +import useOfferRedemptions from './useOfferRedemptions'; import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; -jest.mock('@edx/frontend-platform/config', () => ({ - getConfig: jest.fn(() => ({ - FEATURE_LEARNER_CREDIT_MANAGEMENT: true, - })), -})); -jest.mock('../../../../data/services/EnterpriseDataApiService'); - const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; const TEST_ENTERPRISE_OFFER_ID = 1; -const mockOfferSummary = { - offer_id: TEST_ENTERPRISE_OFFER_ID, - status: 'Open', - enterprise_customer_uuid: TEST_ENTERPRISE_UUID, - amount_of_offer_spent: 200.00, - max_discount: 5000.00, - percent_of_offer_spent: 0.04, - remaining_balance: 4800.00, -}; -const mockEnterpriseOffer = { - id: TEST_ENTERPRISE_OFFER_ID, -}; const mockOfferEnrollments = [{ user_email: 'edx@example.com', course_title: 'Test Course Title', @@ -43,45 +21,11 @@ const mockOfferEnrollmentsResponse = { results: mockOfferEnrollments, }; -describe('useOfferSummary', () => { - it('should handle null enterprise offer', async () => { - const { result } = renderHook(() => useOfferSummary(TEST_ENTERPRISE_UUID)); - - expect(result.current).toEqual({ - offerSummary: undefined, - isLoading: false, - }); - }); - - it('should fetch summary data for enterprise offer', async () => { - EnterpriseDataApiService.fetchEnterpriseOfferSummary.mockResolvedValueOnce({ data: mockOfferSummary }); - const { result, waitForNextUpdate } = renderHook(() => useOfferSummary(TEST_ENTERPRISE_UUID, mockEnterpriseOffer)); - - expect(result.current).toEqual({ - offerSummary: undefined, - isLoading: true, - }); - - await waitForNextUpdate(); +const mockEnterpriseOffer = { + id: TEST_ENTERPRISE_OFFER_ID, +}; - expect(EnterpriseDataApiService.fetchEnterpriseOfferSummary).toHaveBeenCalled(); - const expectedResult = { - totalFunds: 5000, - redeemedFunds: 200, - redeemedFundsExecEd: NaN, - redeemedFundsOcm: NaN, - remainingFunds: 4800, - percentUtilized: 0.04, - offerId: 1, - budgetsSummary: [], - offerType: undefined, - }; - expect(result.current).toEqual({ - offerSummary: expectedResult, - isLoading: false, - }); - }); -}); +jest.mock('../../../../data/services/EnterpriseDataApiService'); describe('useOfferRedemptions', () => { it('should fetch enrollment/redemptions metadata for enterprise offer', async () => { diff --git a/src/components/learner-credit-management/data/hooks/useOfferSummary.test.js b/src/components/learner-credit-management/data/hooks/useOfferSummary.test.js new file mode 100644 index 0000000000..352b104f6b --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useOfferSummary.test.js @@ -0,0 +1,67 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; + +import useOfferSummary from './useOfferSummary'; +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; + +jest.mock('@edx/frontend-platform/config', () => ({ + getConfig: jest.fn(() => ({ + FEATURE_LEARNER_CREDIT_MANAGEMENT: true, + })), +})); +jest.mock('../../../../data/services/EnterpriseDataApiService'); + +const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; +const TEST_ENTERPRISE_OFFER_ID = 1; + +const mockOfferSummary = { + offer_id: TEST_ENTERPRISE_OFFER_ID, + status: 'Open', + enterprise_customer_uuid: TEST_ENTERPRISE_UUID, + amount_of_offer_spent: 200.00, + max_discount: 5000.00, + percent_of_offer_spent: 0.04, + remaining_balance: 4800.00, +}; +const mockEnterpriseOffer = { + id: TEST_ENTERPRISE_OFFER_ID, +}; + +describe('useOfferSummary', () => { + it('should handle null enterprise offer', async () => { + const { result } = renderHook(() => useOfferSummary(TEST_ENTERPRISE_UUID)); + + expect(result.current).toEqual({ + offerSummary: undefined, + isLoading: false, + }); + }); + + it('should fetch summary data for enterprise offer', async () => { + EnterpriseDataApiService.fetchEnterpriseOfferSummary.mockResolvedValueOnce({ data: mockOfferSummary }); + const { result, waitForNextUpdate } = renderHook(() => useOfferSummary(TEST_ENTERPRISE_UUID, mockEnterpriseOffer)); + + expect(result.current).toEqual({ + offerSummary: undefined, + isLoading: true, + }); + + await waitForNextUpdate(); + + expect(EnterpriseDataApiService.fetchEnterpriseOfferSummary).toHaveBeenCalled(); + const expectedResult = { + totalFunds: 5000, + redeemedFunds: 200, + redeemedFundsExecEd: NaN, + redeemedFundsOcm: NaN, + remainingFunds: 4800, + percentUtilized: 0.04, + offerId: 1, + budgetsSummary: [], + offerType: undefined, + }; + expect(result.current).toEqual({ + offerSummary: expectedResult, + isLoading: false, + }); + }); +}); diff --git a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js new file mode 100644 index 0000000000..c36d72ffa0 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; + +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; + +const determineBudgetAssignability = (policyType) => { + const assignableSubsidyAccessPolicyTypes = ['AssignedLearnerCreditAccessPolicy']; + return assignableSubsidyAccessPolicyTypes.includes(policyType); +}; + +/** + * Retrieves a subsidy access policy by UUID from the API. + * + * @param {*} queryKey The queryKey from the associated `useQuery` call. + * @returns The subsidy access policy object, with the `isAssignable` property added. + */ +const getSubsidyAccessPolicy = async ({ queryKey }) => { + const subsidyAccessPolicyUUID = queryKey[2]; + const response = await EnterpriseAccessApiService.retrieveSubsidyAccessPolicy(subsidyAccessPolicyUUID); + const subsidyAccessPolicy = camelCaseObject(response.data); + subsidyAccessPolicy.isAssignable = determineBudgetAssignability(subsidyAccessPolicy.policyType); + return subsidyAccessPolicy; +}; + +const useSubsidyAccessPolicy = (subsidyAccessPolicyId, { queryOptions } = {}) => useQuery({ + queryKey: ['learner-credit-management', 'subsidy-access-policy', subsidyAccessPolicyId], + queryFn: getSubsidyAccessPolicy, + enabled: !!subsidyAccessPolicyId, + ...queryOptions, +}); + +export default useSubsidyAccessPolicy; diff --git a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx new file mode 100644 index 0000000000..4d67e12051 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx @@ -0,0 +1,121 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; + +import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; // Import the hook +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; + +const mockSubsidyAccessPolicyUUID = '9af340a9-48de-4d94-976d-e2282b9eb7f3'; + +// Mock the EnterpriseAccessApiService +jest.mock('../../../../data/services/EnterpriseAccessApiService', () => ({ + retrieveSubsidyAccessPolicy: jest.fn().mockResolvedValue({ + data: { + uuid: '9af340a9-48de-4d94-976d-e2282b9eb7f3', + policyType: 'AssignedLearnerCreditAccessPolicy', + // Other properties... + }, + }), +})); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const wrapper = ({ children }) => ( + {children} +); + +describe('useSubsidyAccessPolicy', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it.each([ + { isAssignable: true }, + { isAssignable: false }, + ])('should fetch and return subsidy access policy (%s)', async ({ isAssignable }) => { + // Mock the policy type in response based on isAssignable + jest.spyOn(EnterpriseAccessApiService, 'retrieveSubsidyAccessPolicy').mockResolvedValueOnce({ + data: { + uuid: mockSubsidyAccessPolicyUUID, + policyType: isAssignable ? 'AssignedLearnerCreditAccessPolicy' : 'PerLearnerCreditSpendLimitAccessPolicy', + // Other properties... + }, + }); + const { result, waitForNextUpdate } = renderHook( + () => useSubsidyAccessPolicy(mockSubsidyAccessPolicyUUID), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(false); + expect(result.current.data).toEqual({ + uuid: mockSubsidyAccessPolicyUUID, + policyType: isAssignable ? 'AssignedLearnerCreditAccessPolicy' : 'PerLearnerCreditSpendLimitAccessPolicy', + isAssignable, + // Other expected properties... + }); + }); + + it('should handle errors gracefully', async () => { + // Mock an error response from the API + jest.spyOn(EnterpriseAccessApiService, 'retrieveSubsidyAccessPolicy').mockRejectedValueOnce(new Error('Mock API Error')); + + const { result, waitForNextUpdate } = renderHook( + () => useSubsidyAccessPolicy(mockSubsidyAccessPolicyUUID), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(true); + expect(result.current.error.message).toBe('Mock API Error'); + }); + + it.each([ + { + subsidyAccessPolicyId: undefined, + expectedData: undefined, + }, + { + subsidyAccessPolicyId: mockSubsidyAccessPolicyUUID, + expectedData: { + uuid: mockSubsidyAccessPolicyUUID, + policyType: 'AssignedLearnerCreditAccessPolicy', + isAssignable: true, + // Other expected properties... + }, + }, + ])('should enable/disable the query based on subsidyAccessPolicyId (%s)', async ({ + subsidyAccessPolicyId, + expectedData, + }) => { + // Mock the policy type in response based on subsidyAccessPolicyId + jest.spyOn(EnterpriseAccessApiService, 'retrieveSubsidyAccessPolicy').mockResolvedValueOnce({ + data: { + uuid: mockSubsidyAccessPolicyUUID, + policyType: 'AssignedLearnerCreditAccessPolicy', + // Other properties... + }, + }); + const { result, waitForNextUpdate } = renderHook(() => useSubsidyAccessPolicy(subsidyAccessPolicyId), { wrapper }); + + if (expectedData) { + await waitForNextUpdate(); + expect(result.current.isLoading).toBe(false); + } else { + expect(result.current.isLoading).toBe(true); + } + + expect(result.current.isInitialLoading).toBe(false); + expect(result.current.isError).toBe(false); + expect(result.current.data).toEqual(expectedData); + }); +}); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 4705d62507..681c421759 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -143,10 +143,11 @@ export const getBudgetStatus = (startDateStr, endDateStr, currentDate = new Date }; }; -export const formatPrice = (price) => { +export const formatPrice = (price, options = {}) => { const USDollar = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', + ...options, }); return USDollar.format(Math.abs(price)); }; diff --git a/src/components/learner-credit-management/search/CatalogSearch.jsx b/src/components/learner-credit-management/search/CatalogSearch.jsx index 43cc7aae8d..c19d87dc50 100644 --- a/src/components/learner-credit-management/search/CatalogSearch.jsx +++ b/src/components/learner-credit-management/search/CatalogSearch.jsx @@ -21,7 +21,7 @@ const CatalogSearch = () => { id="catalogs.enterpriseCatalogs.header" defaultMessage="Budget associated catalog" description="Search dialogue." - tagName="h2" + tagName="h3" />
diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx index 60c20e0262..5127568653 100644 --- a/src/components/learner-credit-management/search/CatalogSearchResults.jsx +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -112,7 +112,6 @@ BaseCatalogSearchResults.defaultProps = { searchResults: { disjunctiveFacetsRefinements: [], nbHits: 0, hits: [] }, error: null, paginationComponent: SearchPagination, - row: null, preview: false, setNoContent: () => {}, }; @@ -134,14 +133,10 @@ BaseCatalogSearchResults.propTypes = { error: PropTypes.shape({ message: PropTypes.string, }), - searchState: PropTypes.shape({ page: PropTypes.number, }).isRequired, paginationComponent: PropTypes.func, - // eslint-disable-next-line react/no-unused-prop-types - row: PropTypes.string, - contentType: PropTypes.string.isRequired, preview: PropTypes.bool, setNoContent: PropTypes.func, }; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index b3a85b84bb..781a012634 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { useParams } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; -import { screen, waitFor } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -11,8 +12,11 @@ import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { act } from 'react-dom/test-utils'; import BudgetDetailPage from '../BudgetDetailPage'; -import { useOfferSummary, useOfferRedemptions } from '../data'; -import { EXEC_ED_OFFER_TYPE } from '../data/constants'; +import { + useSubsidyAccessPolicy, + useOfferRedemptions, + useBudgetContentAssignments, +} from '../data'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; jest.mock('react-router-dom', () => ({ @@ -25,13 +29,28 @@ jest.mock('react-router-dom', () => ({ jest.mock('../data', () => ({ ...jest.requireActual('../data'), - useOfferSummary: jest.fn(), useOfferRedemptions: jest.fn(), + useBudgetContentAssignments: jest.fn(), + useSubsidyAccessPolicy: jest.fn(), })); -useOfferSummary.mockReturnValue({ +useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: { + uuid: 'test-budget-uuid', + policyType: 'PerLearnerSpendCreditAccessPolicy', + displayName: null, + isAssignable: false, + }, +}); +useBudgetContentAssignments.mockReturnValue({ isLoading: false, - offerSummary: null, + contentAssignments: { + count: 0, + results: [], + numPages: 1, + }, + fetchContentAssignments: jest.fn(), }); useOfferRedemptions.mockReturnValue({ isLoading: false, @@ -61,19 +80,12 @@ const initialStoreState = { const mockEnterpriseOfferId = '123'; const mockSubsidyAccessPolicyUUID = 'c17de32e-b80b-468f-b994-85e68fd32751'; -const mockOfferDisplayName = 'Test Enterprise Offer'; -const mockOfferSummary = { - totalFunds: 5000, - redeemedFunds: 200, - remainingFunds: 4800, - percentUtilized: 0.04, - offerType: EXEC_ED_OFFER_TYPE, -}; - const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; +const queryClient = new QueryClient(); + const BudgetDetailPageWrapper = ({ initialState = initialStoreState, enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue, @@ -81,13 +93,15 @@ const BudgetDetailPageWrapper = ({ }) => { const store = getMockStore({ ...initialState }); return ( - - - - - - - + + + + + + + + + ); }; @@ -103,6 +117,29 @@ describe('', () => { }); }); + it.each([ + { displayName: null }, + { displayName: 'Test Budget Display Name' }, + ])('renders budget header data', ({ displayName }) => { + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: { + uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + policyType: 'AssignedLearnerCreditAccessPolicy', + displayName, + }, + }); + const expectedDisplayName = displayName || 'Overview'; + renderWithRouter(); + + // Hero + expect(screen.getByText('Learner Credit Management')); + // Breadcrumb + expect(screen.getByText(expectedDisplayName, { selector: 'li' })); + // Page heading + expect(screen.getByText(expectedDisplayName, { selector: 'h2' })); + }); + it.each([ { budgetId: mockEnterpriseOfferId, @@ -116,20 +153,10 @@ describe('', () => { budgetId, expectedUseOfferRedemptionsArgs, }) => { - const mockOffer = { - id: budgetId, - name: mockOfferDisplayName, - start: '2022-01-01', - end: '2023-01-01', - }; useParams.mockReturnValue({ budgetId, activeTabKey: 'activity', }); - useOfferSummary.mockReturnValue({ - isLoading: false, - offerSummary: mockOfferSummary, - }); useOfferRedemptions.mockReturnValue({ isLoading: false, offerRedemptions: { @@ -139,45 +166,108 @@ describe('', () => { }, fetchOfferRedemptions: jest.fn(), }); - renderWithRouter( - , - ); + renderWithRouter(); expect(useOfferRedemptions).toHaveBeenCalledTimes(1); expect(useOfferRedemptions).toHaveBeenCalledWith(...expectedUseOfferRedemptionsArgs); - // Hero - expect(screen.getByText('Learner Credit Management')); - // Breadcrumb - expect(screen.getByText('Overview')); // Activity tab exists and is active expect(screen.getByText('Activity').getAttribute('aria-selected')).toBe('true'); - // Spend table is visible within Activity tab contents - expect(screen.getByText('No results found')); + // Catalog tab does NOT exist since the budget is not assignable + expect(screen.queryByText('Catalog')).not.toBeInTheDocument(); + + // Spent table is visible within Activity tab contents + const spentSection = within(screen.getByText('Spent').closest('section')); + expect(spentSection.getByText('No results found')).toBeInTheDocument(); + }); + + it('renders with empty assigned table and catalog tab available for assignable budgets', () => { + useParams.mockReturnValue({ + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: { + uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + policyType: 'AssignedLearnerCreditAccessPolicy', + isAssignable: true, + }, + }); + renderWithRouter(); + + // Assigned table is visible within Activity tab contents + const assignedSection = within(screen.getByText('Assigned').closest('section')); + expect(assignedSection.getByText('No results found')).toBeInTheDocument(); + // Catalog tab exists and is NOT active expect(screen.getByText('Catalog').getAttribute('aria-selected')).toBe('false'); }); - it('renders with catalog tab active on initial load', async () => { + it('renders with assigned table data', () => { + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: { + uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + policyType: 'AssignedLearnerCreditAccessPolicy', + isAssignable: true, + }, + }); + const mockLearnerEmail = 'edx@example.com'; + const mockCourseKey = 'edX+DemoX'; + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [ + { + uuid: 'test-uuid', + learnerEmail: mockLearnerEmail, + contentKey: mockCourseKey, + }, + ], + numPages: 1, + currentPage: 1, + }, + }); + renderWithRouter(); + + // Assigned table is visible within Activity tab contents + const assignedSection = within(screen.getByText('Assigned').closest('section')); + expect(assignedSection.queryByText('No results found')).not.toBeInTheDocument(); + expect(assignedSection.getByText(mockLearnerEmail)).toBeInTheDocument(); + const viewCourseCTA = assignedSection.getByText('View course', { selector: 'a' }); + expect(viewCourseCTA).toBeInTheDocument(); + expect(viewCourseCTA.getAttribute('href')).toEqual(`${process.env.ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${mockCourseKey}`); + }); + + it('renders with catalog tab active on initial load for assignable budgets', async () => { useParams.mockReturnValue({ - budgetId: '123', + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', activeTabKey: 'catalog', }); - renderWithRouter( - , - ); + renderWithRouter(); + // Catalog tab exists and is active expect(screen.getByText('Catalog').getAttribute('aria-selected')).toBe('true'); }); - it('hides catalog tab when enterpriseFeatures.topDownAssignmentRealTimeLcm is false', () => { + it('hides catalog tab when budget is not assignable', () => { + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: { + uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + policyType: 'PerLearnerSpendCreditAccessPolicy', + isAssignable: false, + }, + }); + renderWithRouter(); + + // Catalog tab does NOT exist + expect(screen.queryByText('Catalog')).toBeFalsy(); + }); + + it('hides catalog tab when enterpriseFeatures.topDownAssignmentRealTimeLcm', () => { const initialState = { portalConfiguration: { ...initialStoreState.portalConfiguration, @@ -186,13 +276,8 @@ describe('', () => { }, }, }; - renderWithRouter( - , - ); + renderWithRouter(); + // Catalog tab does NOT exist expect(screen.queryByText('Catalog')).toBeFalsy(); }); @@ -202,12 +287,8 @@ describe('', () => { budgetId: '123', activeTabKey: undefined, }); - renderWithRouter( - , - ); + renderWithRouter(); + // Activity tab exists and is active expect(screen.getByText('Activity').getAttribute('aria-selected')).toBe('true'); }); @@ -217,23 +298,21 @@ describe('', () => { budgetId: '123', activeTabKey: 'invalid', }); - renderWithRouter( - , - ); + renderWithRouter(); expect(screen.getByText('404')).toBeInTheDocument(); expect(screen.getByText('something went wrong', { exact: false })).toBeInTheDocument(); }); it('handles user switching to catalog tab', async () => { - renderWithRouter( - , - ); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: { + uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + policyType: 'AssignedLearnerCreditAccessPolicy', + isAssignable: true, + }, + }); + renderWithRouter(); const catalogTab = screen.getByText('Catalog'); await act(async () => { @@ -245,7 +324,12 @@ describe('', () => { }); }); - it('displays loading message while loading data', () => { + it('displays loading message while loading subsidy access policy metadata from API', () => { + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: true, + data: undefined, + }); + renderWithRouter( ', () => { }} />, ); - expect(screen.getByText('Loading')); + + expect(screen.getByText('loading budget details')).toBeInTheDocument(); }); }); diff --git a/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx b/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx index 093363a737..71b7a44970 100644 --- a/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx +++ b/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx @@ -2,18 +2,30 @@ import React from 'react'; import { screen, render, + waitFor, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import '@testing-library/jest-dom/extend-expect'; import EmailAddressTableCell from '../EmailAddressTableCell'; +jest.mock('@edx/frontend-enterprise-utils', () => ({ + ...jest.requireActual('@edx/frontend-enterprise-utils'), + sendEnterpriseTrackEvent: jest.fn(), +})); + const mockStore = configureMockStore(); +const mockEnterpriseUUID = 'test-enterprise-uuid'; +const mockContentAssignmentUUID = 'test-content-assignment-uuid'; +const mockFulfillmentIdentifier = 'test-fulfillment-identifier'; + const mockInitialState = { portalConfiguration: { - enterpriseId: 'test-enterprise', + enterpriseId: mockEnterpriseUUID, }, }; @@ -27,26 +39,67 @@ const EmailAddressTableCellWrapper = ({ ); describe('', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('with email is present, display it', () => { const userEmail = 'edx@example.com'; - const row = { - original: { - userEmail, - }, + const props = { + tableId: 'spent', + userEmail, }; - render(); + render(); expect(screen.getByText(userEmail)); }); - it('without email present, show popover message', async () => { - const row = { - original: { - userEmail: null, - }, + it.each([ + { enterpriseEnrollmentId: 123 }, + { fulfillmentIdentifier: mockFulfillmentIdentifier }, + { contentAssignmentUUID: mockContentAssignmentUUID }, + ])('without email present, show popover message (%s)', async ({ + enterpriseEnrollmentId, + fulfillmentIdentifier, + contentAssignmentUUID, + }) => { + const props = { + tableId: 'spent', + userEmail: null, + enterpriseEnrollmentId, + fulfillmentIdentifier, + contentAssignmentUUID, }; - render(); + render(); expect(screen.getByText('Email hidden')); userEvent.click(screen.getByLabelText('More details')); + + // Verify onEntered Segment event is called when popover opens expect(await screen.findByText('Learner data disabled', { exact: false })); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith( + mockEnterpriseUUID, + 'edx.ui.enterprise.admin_portal.learner-credit-management.spent.email-hidden-popover.opened', + { + enterpriseEnrollmentId, + fulfillmentIdentifier, + contentAssignmentUUID, + }, + ); + + // Verify onExited Segment event is called when popover is closed + userEvent.click(screen.getByLabelText('More details')); + await waitFor(() => { + expect(screen.queryByText('Learner data disabled', { exact: false })).not.toBeInTheDocument(); + }); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith( + mockEnterpriseUUID, + 'edx.ui.enterprise.admin_portal.learner-credit-management.spent.email-hidden-popover.dismissed', + { + enterpriseEnrollmentId, + fulfillmentIdentifier, + contentAssignmentUUID, + }, + ); }); }); diff --git a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx index 785da8899d..09d2f29192 100644 --- a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx +++ b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx @@ -14,10 +14,13 @@ import MultipleBudgetsPage from '../MultipleBudgetsPage'; const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); -const enterpriseId = 'test-enterprise'; +const enterpriseId = 'test-enterprise-uuid'; +const enterpriseSlug = 'test-enterprise-slug'; const initialStore = { portalConfiguration: { enterpriseId, + enterpriseSlug, + enableLearnerPortal: true, }, }; const store = getMockStore({ ...initialStore }); diff --git a/src/components/settings/SettingsTabs.jsx b/src/components/settings/SettingsTabs.jsx index 729ac106bc..f8a10c4cf4 100644 --- a/src/components/settings/SettingsTabs.jsx +++ b/src/components/settings/SettingsTabs.jsx @@ -1,8 +1,4 @@ import React, { useState, useMemo } from 'react'; -import { - QueryClient, - QueryClientProvider, -} from '@tanstack/react-query'; import { Container, Tabs, @@ -31,12 +27,6 @@ import SettingsApiCredentialsTab from './SettingsApiCredentialsTab'; import { features } from '../../config'; import { updatePortalConfigurationEvent } from '../../data/actions/portalConfiguration'; -const queryClient = new QueryClient({ - queries: { - retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis. - }, -}); - const SettingsTabs = ({ enterpriseId, enterpriseSlug, @@ -90,12 +80,10 @@ const SettingsTabs = ({ eventKey={SETTINGS_TABS_VALUES.sso} title={SETTINGS_TAB_LABELS.sso} > - - - + , ); } @@ -145,6 +133,7 @@ const SettingsTabs = ({ , ); } + return initialTabs; }, [ FEATURE_SSO_SETTINGS_TAB, diff --git a/src/components/test/testUtils.jsx b/src/components/test/testUtils.jsx index 02a98208fb..e4a22ba445 100644 --- a/src/components/test/testUtils.jsx +++ b/src/components/test/testUtils.jsx @@ -1,5 +1,4 @@ /* eslint-disable import/no-extraneous-dependencies */ -/* eslint-disable import/prefer-default-export */ import React from 'react'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; @@ -12,6 +11,7 @@ export function renderWithRouter( history = createMemoryHistory({ initialEntries: [route] }), } = {}, ) { + // eslint-disable-next-line react/prop-types const Wrapper = ({ children }) => ( {children} ); diff --git a/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap b/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap index 49c9024f3b..ad4e4dcae7 100644 --- a/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap +++ b/src/containers/Sidebar/__snapshots__/Sidebar.test.jsx.snap @@ -33,7 +33,6 @@ exports[` renders correctly 1`] = ` > renders correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -77,7 +76,6 @@ exports[` renders correctly 1`] = ` > renders correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -121,7 +119,6 @@ exports[` renders correctly 1`] = ` > renders correctly 1`] = ` className="d-flex align-items-center" > renders correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -210,7 +207,6 @@ exports[` renders correctly 1`] = ` > renders correctly 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -275,7 +271,6 @@ exports[` renders correctly when code management is hidden 1`] = ` > renders correctly when code management is hidden 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -319,7 +314,7 @@ exports[` renders correctly when code management is hidden 1`] = ` className="d-flex align-items-center" > renders correctly when code management is hidden 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -364,7 +359,6 @@ exports[` renders correctly when code management is hidden 1`] = ` > renders correctly when code management is hidden 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -429,7 +423,6 @@ exports[` renders correctly when expanded 1`] = ` > renders correctly when expanded 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -471,7 +464,6 @@ exports[` renders correctly when expanded 1`] = ` > renders correctly when expanded 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -513,7 +505,6 @@ exports[` renders correctly when expanded 1`] = ` > renders correctly when expanded 1`] = ` className="d-flex align-items-center" > renders correctly when expanded 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -598,7 +589,6 @@ exports[` renders correctly when expanded 1`] = ` > renders correctly when expanded 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -661,7 +651,6 @@ exports[` renders correctly when expanded by toggle 1`] = ` > renders correctly when expanded by toggle 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -703,7 +692,6 @@ exports[` renders correctly when expanded by toggle 1`] = ` > renders correctly when expanded by toggle 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -745,7 +733,6 @@ exports[` renders correctly when expanded by toggle 1`] = ` > renders correctly when expanded by toggle 1`] = ` className="d-flex align-items-center" > renders correctly when expanded by toggle 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -830,7 +817,6 @@ exports[` renders correctly when expanded by toggle 1`] = ` > renders correctly when expanded by toggle 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/src/data/hooks.js b/src/data/hooks.js index c44b512dd5..5f9e7df7b1 100644 --- a/src/data/hooks.js +++ b/src/data/hooks.js @@ -1,8 +1,4 @@ -import { - useEffect, useMemo, useState, useRef, -} from 'react'; - -import { CONTENT_TYPE_COURSE } from '../components/learner-credit-management/data/constants'; +import { useEffect, useRef } from 'react'; export function useInterval(callback, delay) { const savedCallback = useRef(); @@ -45,12 +41,3 @@ export function useTimeout(callback, delay) { timeoutIdRef.current = null; }, [callback, delay]); } - -export const useSelectedCourse = () => { - const [course, setCourse] = useState(null); - const isCourse = useMemo( - () => course?.contentType === CONTENT_TYPE_COURSE, - [course], - ); - return [course, setCourse, isCourse]; -}; diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index 380428686e..be704fe658 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -1,4 +1,5 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { snakeCaseObject } from '@edx/frontend-platform/utils'; import { configuration } from '../../config'; @@ -141,6 +142,30 @@ class EnterpriseAccessApiService { const url = `${EnterpriseAccessApiService.baseUrl}/coupon-code-requests/overview/?${params.toString()}`; return EnterpriseAccessApiService.apiClient().get(url); } + + /** + * List content assignments for a specific AssignmentConfiguration. + */ + static listContentAssignments(assignmentConfigurationUUID, options = {}) { + const params = new URLSearchParams({ + page: 1, + page_size: 25, + // Only include assignments with allocated or errored states. The table should NOT + // include assignments in the cancelled or accepted states. + state__in: 'allocated,errored', + ...snakeCaseObject(options), + }); + const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/?${params.toString()}`; + return EnterpriseAccessApiService.apiClient().get(url); + } + + /** + * Retrieve a specific subsidy access policy. + */ + static retrieveSubsidyAccessPolicy(subsidyAccessPolicyUUID) { + const url = `${EnterpriseAccessApiService.baseUrl}/subsidy-access-policies/${subsidyAccessPolicyUUID}/`; + return EnterpriseAccessApiService.apiClient().get(url); + } } export default EnterpriseAccessApiService; diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index 485bf7db90..fd1497705d 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -17,6 +17,8 @@ const enterpriseAccessBaseUrl = `${process.env.ENTERPRISE_ACCESS_BASE_URL}`; const mockEnterpriseUUID = 'test-enterprise-id'; const mockLicenseRequestUUID = 'test-license-request-uuid'; const mockCouponCodeRequestUUID = 'test-coupon-code-request-uuid'; +const mockAssignmentConfigurationUUID = 'test-assignment-configuration-uuid'; +const mockSubsidyAccessPolicyUUID = 'test-subsidy-access-policy-uuid'; describe('EnterpriseAccessApiService', () => { beforeEach(() => { @@ -131,4 +133,23 @@ describe('EnterpriseAccessApiService', () => { subsidy_type: SUPPORTED_SUBSIDY_TYPES.coupon, }); }); + + test('listContentAssignments calls enterprise-access to fetch content assignments', () => { + EnterpriseAccessApiService.listContentAssignments(mockAssignmentConfigurationUUID); + const expectedParams = new URLSearchParams({ + page: 1, + page_size: 25, + state__in: 'allocated,errored', + }).toString(); + expect(axios.get).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/?${expectedParams}`, + ); + }); + + test('retrieveSubsidyAccessPolicy calls enterprise-access to fetch subsidy access policy', () => { + EnterpriseAccessApiService.retrieveSubsidyAccessPolicy(mockSubsidyAccessPolicyUUID); + expect(axios.get).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/subsidy-access-policies/${mockSubsidyAccessPolicyUUID}/`, + ); + }); }); From 080692cd7e71a0290e364116b80d094ec3d4b7d9 Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:17:14 -0700 Subject: [PATCH 043/124] fix: bug fix for remind/revoke functions in license management table (#1063) * fix: bug fix for remind/revoke functions in license management table * fix: syntax * fix: added test coverage --- .../tests/LicenseManagementRemindModal.test.jsx | 6 +++++- .../tests/LicenseManagementRevokeModal.test.jsx | 6 +++++- .../LicenseManagementTableActionColumn.jsx | 6 ++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRemindModal.test.jsx b/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRemindModal.test.jsx index 73fb27b88e..d7fb86876a 100644 --- a/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRemindModal.test.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRemindModal.test.jsx @@ -31,10 +31,12 @@ jest.mock('../../../../../data/services/LicenseManagerAPIService', () => ({ })); const onSubmitMock = jest.fn(); +const onSuccessMock = jest.fn(); + const basicProps = { isOpen: true, onClose: () => {}, - onSuccess: () => {}, + onSuccess: onSuccessMock, onSubmit: onSubmitMock, subscription: { uuid: 'lorem', @@ -110,6 +112,7 @@ describe('', () => { const button = screen.getByText('Remind (1)'); await act(async () => { userEvent.click(button); }); expect(onSubmitMock).toBeCalledTimes(1); + expect(onSuccessMock).toBeCalledTimes(1); expect(screen.queryByText('Remind (1)')).toBeFalsy(); expect(screen.queryByText('Done')).toBeTruthy(); @@ -127,6 +130,7 @@ describe('', () => { const button = screen.getByText('Remind (1)'); await act(async () => { userEvent.click(button); }); expect(onSubmitMock).toBeCalledTimes(1); + expect(onSuccessMock).toBeCalledTimes(0); await waitFor(() => { expect(screen.getByRole('alert')).toBeTruthy(); diff --git a/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRevokeModal.test.jsx b/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRevokeModal.test.jsx index d9bb7b769c..309897217d 100644 --- a/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRevokeModal.test.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementModals/tests/LicenseManagementRevokeModal.test.jsx @@ -23,10 +23,12 @@ jest.mock('../../../../../data/services/LicenseManagerAPIService', () => ({ })); const onSubmitMock = jest.fn(); +const onSuccessMock = jest.fn(); + const basicProps = { isOpen: true, onClose: () => {}, - onSuccess: () => {}, + onSuccess: onSuccessMock, onSubmit: onSubmitMock, subscription: { uuid: 'lorem', @@ -101,6 +103,7 @@ describe('', () => { const button = screen.getByText('Revoke (1)'); await act(async () => { userEvent.click(button); }); expect(onSubmitMock).toBeCalledTimes(1); + expect(onSuccessMock).toBeCalledTimes(1); expect(screen.queryByText('Revoke (1)')).toBeFalsy(); expect(screen.getByText('Done')); @@ -117,6 +120,7 @@ describe('', () => { const button = screen.getByText('Revoke (1)'); await act(async () => { userEvent.click(button); }); expect(onSubmitMock).toBeCalledTimes(1); + expect(onSuccessMock).toBeCalledTimes(0); await waitFor(() => { expect(screen.getByRole('alert')).toBeTruthy(); diff --git a/src/components/subscriptions/licenses/LicenseManagementTable/LicenseManagementTableActionColumn.jsx b/src/components/subscriptions/licenses/LicenseManagementTable/LicenseManagementTableActionColumn.jsx index 42878231d1..cbf889203c 100644 --- a/src/components/subscriptions/licenses/LicenseManagementTable/LicenseManagementTableActionColumn.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementTable/LicenseManagementTableActionColumn.jsx @@ -65,12 +65,14 @@ const LicenseManagementTableActionColumn = ({ const handleRevokeSuccess = () => { setRevokeModal(modalZeroState); - onRevokeSuccess(clearSelection)(); + clearSelection(); + onRevokeSuccess(); }; const handleRemindSuccess = () => { setRemindModal(modalZeroState); - onRemindSuccess(clearSelection)(); + clearSelection(); + onRemindSuccess(); }; const handleRevokeSubmit = () => { From 8b12828a859b89d40667fccc220cc272e60d7e1b Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 23 Oct 2023 11:29:27 -0400 Subject: [PATCH 044/124] chore: Update to the new version of brand-openedx in the new scope. (#1064) Part of https://github.com/openedx/axim-engineering/issues/23 This updates the brand alias to point to the package at the `openedx` scope. This does not impact imports because this package is used via an alias. --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index d37f4b5d06..0a62f3ad47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0", "dependencies": { "@babel/plugin-transform-runtime": "7.12.1", - "@edx/brand": "npm:@edx/brand-openedx@1.2.0", + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-enterprise-catalog-search": "4.2.0", "@edx/frontend-enterprise-hotjar": "1.3.0", "@edx/frontend-enterprise-logistration": "3.2.0", @@ -2101,10 +2101,10 @@ } }, "node_modules/@edx/brand": { - "name": "@edx/brand-openedx", - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@edx/brand-openedx/-/brand-openedx-1.2.0.tgz", - "integrity": "sha512-r4PDN3rCgDsLovW44ayxoNNHgG5I4Rvss6MG5CrQEX4oW8YhQVEod+jJtwR5vi0mFLN2GIaMlDpd7iIy03VqXg==" + "name": "@openedx/brand-openedx", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@openedx/brand-openedx/-/brand-openedx-1.2.2.tgz", + "integrity": "sha512-mBvxR7aB9290j9+h3d/9G8VkG1b8ecLSmlxc0vskfm7DL/fKUzFmHAj3PI7Z4kkwCQOL4QT5mJHJKC0ZFf7qvQ==" }, "node_modules/@edx/browserslist-config": { "version": "1.0.0", diff --git a/package.json b/package.json index 324366e66f..d042436e6c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "license": "AGPL-3.0", "dependencies": { "@babel/plugin-transform-runtime": "7.12.1", - "@edx/brand": "npm:@edx/brand-openedx@1.2.0", + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-enterprise-catalog-search": "4.2.0", "@edx/frontend-enterprise-hotjar": "1.3.0", "@edx/frontend-enterprise-logistration": "3.2.0", From b8110af99cacb6cc18ab6b4d88e050bc0f5fcaf8 Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:49:10 -0700 Subject: [PATCH 045/124] feat: add course title to assigned table (#1066) * feat: add course title to assigned table * fix: unit test * fix: add fall back 'view course' text --- .../AssignmentDetailsTableCell.jsx | 4 +- .../tests/BudgetDetailPage.test.jsx | 41 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx index 92826cb93f..21a2f6ddeb 100644 --- a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx @@ -20,9 +20,10 @@ const AssignmentDetailsTableCell = ({ row, enterpriseSlug }) => { className="x-small" destination={`${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${row.original.contentKey}`} target="_blank" + showLaunchIcon={false} isInline > - View course + {row.original?.contentTitle || 'View Course'}
@@ -39,6 +40,7 @@ AssignmentDetailsTableCell.propTypes = { uuid: PropTypes.string.isRequired, learnerEmail: PropTypes.string.isRequired, contentKey: PropTypes.string.isRequired, + contentTitle: PropTypes.string, }).isRequired, }).isRequired, enterpriseSlug: PropTypes.string, diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 781a012634..1e32550883 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -214,6 +214,7 @@ describe('', () => { }, }); const mockLearnerEmail = 'edx@example.com'; + const mockContentTitle = 'edx Demo'; const mockCourseKey = 'edX+DemoX'; useBudgetContentAssignments.mockReturnValue({ isLoading: false, @@ -224,6 +225,7 @@ describe('', () => { uuid: 'test-uuid', learnerEmail: mockLearnerEmail, contentKey: mockCourseKey, + contentTitle: mockContentTitle, }, ], numPages: 1, @@ -236,7 +238,44 @@ describe('', () => { const assignedSection = within(screen.getByText('Assigned').closest('section')); expect(assignedSection.queryByText('No results found')).not.toBeInTheDocument(); expect(assignedSection.getByText(mockLearnerEmail)).toBeInTheDocument(); - const viewCourseCTA = assignedSection.getByText('View course', { selector: 'a' }); + const viewCourseCTA = assignedSection.getByText('edx Demo', { selector: 'a' }); + expect(viewCourseCTA).toBeInTheDocument(); + expect(viewCourseCTA.getAttribute('href')).toEqual(`${process.env.ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${mockCourseKey}`); + }); + + it('renders with assigned table data "View Course" hyperlink default when content title is null', () => { + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: { + uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + policyType: 'AssignedLearnerCreditAccessPolicy', + isAssignable: true, + }, + }); + const mockLearnerEmail = 'edx@example.com'; + const mockCourseKey = 'edX+DemoX'; + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [ + { + uuid: 'test-uuid', + learnerEmail: mockLearnerEmail, + contentKey: mockCourseKey, + }, + ], + numPages: 1, + currentPage: 1, + }, + }); + renderWithRouter(); + + // Assigned table is visible within Activity tab contents + const assignedSection = within(screen.getByText('Assigned').closest('section')); + expect(assignedSection.queryByText('No results found')).not.toBeInTheDocument(); + expect(assignedSection.getByText(mockLearnerEmail)).toBeInTheDocument(); + const viewCourseCTA = assignedSection.getByText('View Course', { selector: 'a' }); expect(viewCourseCTA).toBeInTheDocument(); expect(viewCourseCTA.getAttribute('href')).toEqual(`${process.env.ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${mockCourseKey}`); }); From 3a2e08150b34def18bcac1ce2e5de53a07ac9ba9 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 27 Oct 2023 13:48:40 -0400 Subject: [PATCH 046/124] feat: learner credit management activity tab empty states (#1068) --- docs/decisions/0006-tanstack-react-query.rst | 71 ++++ .../AssignMoreCoursesEmptyStateMinimal.jsx | 51 +++ .../AssignmentDetailsTableCell.jsx | 5 +- .../BudgetAssignmentsTable.jsx | 6 +- .../BudgetCard-V2.jsx | 2 +- .../BudgetDetailActivityTabContents.jsx | 71 ++-- .../BudgetDetailAssignments.jsx | 52 ++- .../BudgetDetailRedemptions.jsx | 65 ++-- .../NoBudgetActivityEmptyState.jsx | 95 ++++++ .../SpendTableEnrollmentDetails.jsx | 2 +- .../assets/confirmSpend.svg | 128 +++++++ .../assets/findTheRightCourse.svg | 42 +++ .../assets/nameYourLearners.svg | 77 +++++ .../data/constants.js | 10 + .../data/hooks/index.js | 3 + .../hooks/useBudgetContentAssignments.test.js | 2 +- .../hooks/useBudgetDetailActivityOverview.js | 22 ++ .../useBudgetDetailActivityOverview.test.jsx | 160 +++++++++ .../data/hooks/useIsLargeOrGreater.js | 5 + .../data/hooks/usePathToCatalogTab.js | 12 + .../data/hooks/useSubsidyAccessPolicy.js | 11 +- .../hooks/useSubsidyAccessPolicy.test.jsx | 5 + .../data/tests/constants.js | 25 ++ .../learner-credit-management/data/utils.js | 100 ++++++ .../AssignMoreCoursesEmptyStateMinimal.scss | 14 + .../tests/BudgetDetailPage.test.jsx | 318 +++++++++++++----- src/index.scss | 1 + 27 files changed, 1165 insertions(+), 190 deletions(-) create mode 100644 docs/decisions/0006-tanstack-react-query.rst create mode 100644 src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx create mode 100644 src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx create mode 100644 src/components/learner-credit-management/assets/confirmSpend.svg create mode 100644 src/components/learner-credit-management/assets/findTheRightCourse.svg create mode 100644 src/components/learner-credit-management/assets/nameYourLearners.svg create mode 100644 src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.js create mode 100644 src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx create mode 100644 src/components/learner-credit-management/data/hooks/useIsLargeOrGreater.js create mode 100644 src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js create mode 100644 src/components/learner-credit-management/data/tests/constants.js create mode 100644 src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss diff --git a/docs/decisions/0006-tanstack-react-query.rst b/docs/decisions/0006-tanstack-react-query.rst new file mode 100644 index 0000000000..6b5523124c --- /dev/null +++ b/docs/decisions/0006-tanstack-react-query.rst @@ -0,0 +1,71 @@ +6. Adopting ``@tanstack/react-query`` for data fetching and client-side caching +============================================================================= + +Status +****** + +Accepted (October 2023) + +Context +******* + +The ``frontend-app-admin-portal`` MFE currently relies on custom state variables when integrating with an API (e.g., managing loading states). As a result, there is generally a fair amount of boilerplate involved with each API integration. Additionally, the current approach does not provide any client-side caching out-of-the-box or any other best-in-class features like automatic query retries, which can lead to heavy reliance on approaches such as React Context or Redux in order to pass data returned by asynchronous API calls throughout the application. + +The existing approach of heavily relying on React Context providers has resulted in many nested context providers that can make it difficult for contributors to understand what data is available to them from which context provider. Additionally, the reliance on context providers means accepting some performance risk that all components nested under a context provider will re-render whenever any value within the context provider changes, which can lead to performance issues if the context provider is wrapping a large number of components if not mitigated with techniques like ``React.memo``, ``useMemo``, or context selectors. + +Decisions +********* + +We will instead rely on ``@tanstack/react-query`` for data fetching and client-side caching. This library provides a number of benefits over the existing approach, including: + +* Using ``@tanstack/react-query`` will allow us to avoid writing a significant amount of boilerplate code that is currently required to integrate with an API. We can rely on the library to handle loading states, error states, and other common API integration concerns. +* Flexible client-side caching, which will allow us to avoid using custom caching logic provided by ``@edx/frontend-platform``, which is not as full-featured. +* A number of other features out-of-the-box, including automatic query retries on failed network requests, which will allow us to avoid writing custom logic to handle these scenarios. +* Using ``@tanstack/react-query`` will allow us to avoid relying on React Context providers to pass data returned by asynchronous API calls throughout the application. Instead, we can rely on the library to handle this for us via custom hooks. For example, calling ``useQuery`` twice within the rendering lifecycle will not result in duplicate API calls. Instead, the library will first make the initial network call and then store its response in a client-side cache. The second call to ``useQuery`` will then return the cached response instead of making a duplicate network call. The cache invalidation is customizable globally for the application or by query. + + +We will adopt query key factories to manage the implementation of query keys such that cache invalidation for independent features (e.g., Learner Credit Management) can be managed with adequate granularity. For example: + +:: + + // Query Key Factory + export const learnerCreditManagementQueryKeys = { + all: ['learner-credit-management'], + budgets: () => [...learnerCreditManagementQueryKeys.all, 'budgets'], + budget: (budgetId) => [...learnerCreditManagementQueryKeys.all, 'budget', budgetId], + budgetActivity: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'activity'], + budgetActivityOverview: (budgetId) => [...learnerCreditManagementQueryKeys.budgetActivity(budgetId), 'overview'], + }; + +By having a query key factory as suggested above, contributors may have a structured way to manage query keys for a given feature. This approach enabled granular control over cache invalidation, query prefetching, etc. Using the above example, one could invalidate the query cache for the entire ``['learner-credit-management']`` feature or opt to only invalidate the query cache for specific individual queries or even groups of queries: + +:: + + // Remove everything related to the learner credit management feature + queryClient.removeQueries({ + queryKey: learnerCreditManagementQueryKeys.all, + }) + + // Invalidate all queries supporting the budget detail page route + queryClient.invalidateQueries({ + queryKey: learnerCreditManagementQueryKeys.budget(budgetId), + }) + + // Invalidate the budget detail page route's activity tab's overview query + queryClient.invalidateQueries({ + queryKey: learnerCreditManagementQueryKeys.budgetActivityOverview(budgetId), + }) + +The recommendation for new asynchronous network calls moving forward is to use ``@tanstack/react-query``. Additionally, it's recommended to incrementally migrate existing network calls to use ``@tanstack/react-query``. This will allow us to avoid a large refactoring effort and instead migrate to the library over time as we touch existing network calls. + +Consequences +************ + +The entire application is wrapped within ``QueryClientProvider``, which contains a default configuration to at least extend the ``staleTime`` to be 20 seconds. This change in default behavior is to enable the use of ``@tanstack/react-query`` as a state manager. Instead of queries becoming instantly stale after success (i.e., ``staleTime: 0``) where network calls would be re-fetched on all window refocuses and component mounts, etc., having a ``staleTime`` of 20 seconds will keep the data fresh for 20 seconds before the query becomes stale, preventing unnecessary API calls (e.g., when calling duplicate ``useQuery`` hooks in the same rendering lifecycle). + +By adopting ``@tanstack/react-query``, by default, queries made with ``useQuery`` will have some automatic background refetching behavior baked in. It's recommended to consider whether any specific queries should override/extend the default options provided by the library. For example, some specific queries may not want the automatic refetches, etc. that are provided by default. + +Alternatives Considered +*********************** + +* In order to enable the same pattern of relying on ``@tanstack/react-query`` as a state manager to avoid additional context providers, it was considered whether we could make heavier use of the client-side caching provided by ``@edx/frontend-platform``. This was decided against as it is not as full-featured as ``@tanstack/react-query`` when it comes to caching alone. Additionally, it does not provide any other features that are provided by ``@tanstack/react-query``, which will enable more efficient API integrations moving forward. diff --git a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx new file mode 100644 index 0000000000..d51595c5e9 --- /dev/null +++ b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Button, Card } from '@edx/paragon'; + +import { + formatDate, formatPrice, useBudgetId, usePathToCatalogTab, useSubsidyAccessPolicy, +} from './data'; +import nameYourLearner from './assets/nameYourLearners.svg'; + +const AssignMoreCoursesEmptyStateMinimal = () => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const pathToCatalogTab = usePathToCatalogTab(); + + if (!subsidyAccessPolicy) { + return null; + } + + const availableBalance = subsidyAccessPolicy.aggregates.spendAvailableUsd; + const subsidyExpirationDate = subsidyAccessPolicy.subsidyExpirationDatetime; + + return ( + + + + +
+

Assign more courses to maximize your budget.

+ + Your budget's available balance of {formatPrice(availableBalance)} will + expire on {formatDate(subsidyExpirationDate)}. + +
+
+
+ + + +
+ ); +}; + +export default AssignMoreCoursesEmptyStateMinimal; diff --git a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx index 21a2f6ddeb..b04f3b0a9b 100644 --- a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx @@ -20,10 +20,9 @@ const AssignmentDetailsTableCell = ({ row, enterpriseSlug }) => { className="x-small" destination={`${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${row.original.contentKey}`} target="_blank" - showLaunchIcon={false} isInline > - {row.original?.contentTitle || 'View Course'} + {row.original.contentTitle || 'View Course'}
@@ -38,7 +37,7 @@ AssignmentDetailsTableCell.propTypes = { row: PropTypes.shape({ original: PropTypes.shape({ uuid: PropTypes.string.isRequired, - learnerEmail: PropTypes.string.isRequired, + learnerEmail: PropTypes.string, contentKey: PropTypes.string.isRequired, contentTitle: PropTypes.string, }).isRequired, diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index 5b2b94b5b0..cbb7596feb 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -47,9 +47,9 @@ const BudgetAssignmentsTable = ({ filters: [], }} fetchData={fetchTableData} - data={tableData.results} - itemCount={tableData.count} - pageCount={tableData.numPages} + data={tableData?.results || []} + itemCount={tableData?.count || 0} + pageCount={tableData?.numPages || 1} EmptyTableComponent={CustomDataTableEmptyState} /> ); diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx index 3be534f1dd..41ae121150 100644 --- a/src/components/learner-credit-management/BudgetCard-V2.jsx +++ b/src/components/learner-credit-management/BudgetCard-V2.jsx @@ -73,7 +73,7 @@ BudgetCard.propTypes = { }).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, - offerType: PropTypes.string.isRequired, + offerType: PropTypes.oneOf(Object.values(BUDGET_TYPES)).isRequired, displayName: PropTypes.string, }; diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 28a152fce7..517c4e4954 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -1,56 +1,51 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Stack } from '@edx/paragon'; +import { Stack, Skeleton } from '@edx/paragon'; import BudgetDetailRedemptions from './BudgetDetailRedemptions'; import BudgetDetailAssignments from './BudgetDetailAssignments'; -import { - useOfferRedemptions, - useBudgetContentAssignments, - useBudgetId, - useSubsidyAccessPolicy, -} from './data'; +import { useBudgetDetailActivityOverview } from './data'; +import NoBudgetActivityEmptyState from './NoBudgetActivityEmptyState'; -const BudgetDetailActivityTabContents = ({ - enterpriseUUID, - enterpriseFeatures, -}) => { - const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId(); +const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) => { + const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm; const { - data: subsidyAccessPolicy, - } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + isLoading: isBudgetActivityOverviewLoading, + data: budgetActivityOverview, + } = useBudgetDetailActivityOverview({ + enterpriseUUID, + isTopDownAssignmentEnabled, + }); - const isTopDownAssignmentEnabled = enterpriseFeatures?.topDownAssignmentRealTimeLcm; + if (isBudgetActivityOverviewLoading || !budgetActivityOverview) { + return ( + <> + + loading budget activity overview + + ); + } - const { - isLoading: isLoadingOfferRedemptions, - offerRedemptions, - fetchOfferRedemptions, - } = useOfferRedemptions(enterpriseUUID, enterpriseOfferId, subsidyAccessPolicyId); + const hasContentAssignments = !!budgetActivityOverview.contentAssignments?.count > 0; + const hasSpentTransactions = !!budgetActivityOverview.spentTransactions?.count > 0; - const { - isLoading: isLoadingContentAssignments, - contentAssignments, - fetchContentAssignments, - } = useBudgetContentAssignments({ - assignmentConfigurationUUID: subsidyAccessPolicy?.assignmentConfiguration?.uuid, - isEnabled: subsidyAccessPolicy?.isAssignable && isTopDownAssignmentEnabled, - }); + // If there is no activity whatsoever (no assignments, no spent transactions), show the + // full empty state. + if (!hasContentAssignments && !hasSpentTransactions) { + return ( + + ); + } + // Otherwise, render the contents of the "Activity" tab. return ( - + ); }; @@ -64,7 +59,7 @@ BudgetDetailActivityTabContents.propTypes = { enterpriseUUID: PropTypes.string.isRequired, enterpriseFeatures: PropTypes.shape({ topDownAssignmentRealTimeLcm: PropTypes.bool, - }), + }).isRequired, }; export default connect(mapStateToProps)(BudgetDetailActivityTabContents); diff --git a/src/components/learner-credit-management/BudgetDetailAssignments.jsx b/src/components/learner-credit-management/BudgetDetailAssignments.jsx index f4ba6eb8f2..218ac84d5d 100644 --- a/src/components/learner-credit-management/BudgetDetailAssignments.jsx +++ b/src/components/learner-credit-management/BudgetDetailAssignments.jsx @@ -1,17 +1,40 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + import BudgetAssignmentsTable from './BudgetAssignmentsTable'; +import AssignMoreCoursesEmptyStateMinimal from './AssignMoreCoursesEmptyStateMinimal'; +import { useBudgetContentAssignments, useBudgetId, useSubsidyAccessPolicy } from './data'; const BudgetDetailAssignments = ({ - isEnabled, - isLoading, - tableData, - fetchTableData, + hasContentAssignments, + hasSpentTransactions, + enterpriseFeatures, }) => { - if (!isEnabled) { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const isAssignableBudget = !!subsidyAccessPolicy?.isAssignable; + const assignmentConfigurationUUID = subsidyAccessPolicy?.assignmentConfiguration?.uuid; + const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm; + const { + isLoading, + contentAssignments, + fetchContentAssignments, + } = useBudgetContentAssignments({ + isEnabled: isAssignableBudget && hasContentAssignments, + assignmentConfigurationUUID, + }); + + if (!isTopDownAssignmentEnabled || !isAssignableBudget) { return null; } + if (!hasContentAssignments && hasSpentTransactions) { + return ( + + ); + } + return (

Assigned

@@ -21,18 +44,23 @@ const BudgetDetailAssignments = ({

); }; +const mapStateToProps = state => ({ + enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, +}); + BudgetDetailAssignments.propTypes = { - isEnabled: PropTypes.bool.isRequired, - isLoading: PropTypes.bool.isRequired, - tableData: PropTypes.shape().isRequired, - fetchTableData: PropTypes.func.isRequired, + hasContentAssignments: PropTypes.bool.isRequired, + hasSpentTransactions: PropTypes.bool.isRequired, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool, + }).isRequired, }; -export default BudgetDetailAssignments; +export default connect(mapStateToProps)(BudgetDetailAssignments); diff --git a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx index bccf80aa6c..67fd14f087 100644 --- a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx +++ b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx @@ -3,55 +3,38 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; +import { useBudgetId, useOfferRedemptions } from './data'; -const BudgetDetailRedemptions = ({ - isLoading, - offerRedemptions, - fetchOfferRedemptions, - enterpriseUUID, - enterpriseSlug, - enableLearnerPortal, -}) => ( -
-

Spent

-

- Spent activity is driven by completed enrollments. Enrollment data is automatically updated every 12 hours. - Come back later to view more recent enrollments. -

- -
-); +const BudgetDetailRedemptions = ({ enterpriseUUID }) => { + const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId(); + const { + isLoading, + offerRedemptions, + fetchOfferRedemptions, + } = useOfferRedemptions(enterpriseUUID, enterpriseOfferId, subsidyAccessPolicyId); + + return ( +
+

Spent

+

+ Spent activity is driven by completed enrollments. Enrollment data is automatically updated every 12 hours. + Come back later to view more recent enrollments. +

+ +
+ ); +}; const mapStateToProps = state => ({ enterpriseUUID: state.portalConfiguration.enterpriseId, - enterpriseSlug: state.portalConfiguration.enterpriseSlug, - enableLearnerPortal: state.portalConfiguration.enableLearnerPortal, }); BudgetDetailRedemptions.propTypes = { enterpriseUUID: PropTypes.string.isRequired, - enterpriseSlug: PropTypes.string.isRequired, - enableLearnerPortal: PropTypes.bool.isRequired, - isLoading: PropTypes.bool.isRequired, - offerRedemptions: PropTypes.shape({ - results: PropTypes.arrayOf(PropTypes.shape({ - userEmail: PropTypes.string, - courseTitle: PropTypes.string.isRequired, - courseListPrice: PropTypes.number.isRequired, - enrollmentDate: PropTypes.string.isRequired, - courseProductLine: PropTypes.string.isRequired, - })), - itemCount: PropTypes.number.isRequired, - pageCount: PropTypes.number.isRequired, - }).isRequired, - fetchOfferRedemptions: PropTypes.func.isRequired, }; export default connect(mapStateToProps)(BudgetDetailRedemptions); diff --git a/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx b/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx new file mode 100644 index 0000000000..3d6f074208 --- /dev/null +++ b/src/components/learner-credit-management/NoBudgetActivityEmptyState.jsx @@ -0,0 +1,95 @@ +import React from 'react'; +import classNames from 'classnames'; +import { + Button, Card, Row, Col, +} from '@edx/paragon'; +import { Link } from 'react-router-dom'; + +import { useIsLargeOrGreater, usePathToCatalogTab } from './data'; +import nameYourLearners from './assets/nameYourLearners.svg'; +import findTheRightCourse from './assets/findTheRightCourse.svg'; +import confirmSpend from './assets/confirmSpend.svg'; + +const FindTheRightCourseIllustration = (props) => ( + +); + +const NameYourLearnersIllustration = (props) => ( + +); + +const ConfirmSpendIllustration = (props) => ( + +); + +const NoBudgetActivityEmptyState = () => { + const pathToCatalogTab = usePathToCatalogTab(); + const isLargeOrGreater = useIsLargeOrGreater(); + + return ( + + +

+ No budget activity yet? Assign a course! +

+ {isLargeOrGreater && ( + + + + + + + + + + + + )} +
+ + + + {!isLargeOrGreater && } +

+ 01 + Find the right course +

+ + Check out your budget's catalog of courses and select the course you + want to assign to learners. + + + + {!isLargeOrGreater && } +

+ 02 + Name your learners +

+ + You will be prompted to enter email addresses for the learner or + learners you want to assign. + + + + {!isLargeOrGreater && } +

+ 03 + Confirm spend +

+ + Once confirmed, the total cost will be deducted from your budget, + and you can track your spending right here! + + +
+ + + + + +
+
+ ); +}; + +export default NoBudgetActivityEmptyState; diff --git a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx index dafd44b88f..a0d61f8dda 100644 --- a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx +++ b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx @@ -39,7 +39,7 @@ const rowPropType = PropTypes.shape({ original: PropTypes.shape({ courseKey: PropTypes.string.isRequired, courseTitle: PropTypes.string.isRequired, - userEmail: PropTypes.string.isRequired, + userEmail: PropTypes.string, enterpriseEnrollmentId: PropTypes.number, fulfillmentIdentifier: PropTypes.string, }).isRequired, diff --git a/src/components/learner-credit-management/assets/confirmSpend.svg b/src/components/learner-credit-management/assets/confirmSpend.svg new file mode 100644 index 0000000000..826f4eea85 --- /dev/null +++ b/src/components/learner-credit-management/assets/confirmSpend.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/learner-credit-management/assets/findTheRightCourse.svg b/src/components/learner-credit-management/assets/findTheRightCourse.svg new file mode 100644 index 0000000000..5834e5fd6b --- /dev/null +++ b/src/components/learner-credit-management/assets/findTheRightCourse.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/learner-credit-management/assets/nameYourLearners.svg b/src/components/learner-credit-management/assets/nameYourLearners.svg new file mode 100644 index 0000000000..e015abf48a --- /dev/null +++ b/src/components/learner-credit-management/assets/nameYourLearners.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 8db03bb0ce..8a93ee6f6d 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -37,3 +37,13 @@ export const EXEC_COURSE_TYPE = 'executive-education-2u'; // Number of items to display per page in Budget Detail assignment/spend tables export const PAGE_SIZE = 25; export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array + +// Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`. +// Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. +export const learnerCreditManagementQueryKeys = { + all: ['learner-credit-management'], + budgets: () => [...learnerCreditManagementQueryKeys.all, 'budgets'], + budget: (budgetId) => [...learnerCreditManagementQueryKeys.all, 'budget', budgetId], + budgetActivity: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'activity'], + budgetActivityOverview: (budgetId) => [...learnerCreditManagementQueryKeys.budgetActivity(budgetId), 'overview'], +}; diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index 3b21c9be3f..b7c35f509f 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -4,3 +4,6 @@ export { default as useOfferRedemptions } from './useOfferRedemptions'; export { default as useBudgetContentAssignments } from './useBudgetContentAssignments'; export { default as useBudgetId } from './useBudgetId'; export { default as useSubsidyAccessPolicy } from './useSubsidyAccessPolicy'; +export { default as usePathToCatalogTab } from './usePathToCatalogTab'; +export { default as useBudgetDetailActivityOverview } from './useBudgetDetailActivityOverview'; +export { default as useIsLargeOrGreater } from './useIsLargeOrGreater'; diff --git a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js index 0ede9195bd..a0715f22ef 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js +++ b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks'; -import useBudgetContentAssignments from './useBudgetContentAssignments'; // Import the hook +import useBudgetContentAssignments from './useBudgetContentAssignments'; import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; describe('useBudgetContentAssignments', () => { diff --git a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.js b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.js new file mode 100644 index 0000000000..8b7a945e84 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; +import { retrieveBudgetDetailActivityOverview } from '../utils'; +import useBudgetId from './useBudgetId'; +import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; +import { learnerCreditManagementQueryKeys } from '../constants'; + +const useBudgetDetailActivityOverview = ({ enterpriseUUID, isTopDownAssignmentEnabled }) => { + const { budgetId, subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + return useQuery({ + queryKey: learnerCreditManagementQueryKeys.budgetActivityOverview(budgetId), + queryFn: (args) => retrieveBudgetDetailActivityOverview({ + ...args, + budgetId, + subsidyAccessPolicy, + enterpriseUUID, + isTopDownAssignmentEnabled, + }), + }); +}; + +export default useBudgetDetailActivityOverview; diff --git a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx new file mode 100644 index 0000000000..cdb3b3e9bf --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx @@ -0,0 +1,160 @@ +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; + +import useBudgetDetailActivityOverview from './useBudgetDetailActivityOverview'; +import useBudgetId from './useBudgetId'; +import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; +import { + mockAssignableSubsidyAccessPolicy, + mockPerLearnerSpendLimitSubsidyAccessPolicy, + mockEnterpriseOfferId, + mockSubsidyAccessPolicyUUID, +} from '../tests/constants'; + +jest.mock('./useBudgetId'); +jest.mock('./useSubsidyAccessPolicy'); + +const mockEnterpriseUUID = 'mock-enterprise-uuid'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const wrapper = ({ children }) => ( + + {children} + +); + +describe('useBudgetDetailActivityOverview', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('handles when budget is an enterprise offer id (not a subsidy access policy uuid)', async () => { + useBudgetId.mockReturnValue({ + budgetId: mockEnterpriseOfferId, + subsidyAccessPolicyId: undefined, + }); + useSubsidyAccessPolicy.mockReturnValue({ data: undefined }); + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + const mockFetchCourseEnrollments = jest.spyOn(EnterpriseDataApiService, 'fetchCourseEnrollments'); + mockFetchCourseEnrollments.mockResolvedValue({ + data: { + count: 1, + results: [{ id: 'mock-course-enrollment-id' }], + }, + }); + + const { result, waitForNextUpdate } = renderHook( + () => useBudgetDetailActivityOverview({ + enterpriseUUID: mockEnterpriseUUID, + isTopDownAssignmentEnabled: true, + }), + { wrapper }, + ); + + expect(useSubsidyAccessPolicy).toHaveBeenCalledWith(undefined); + + expect(mockListContentAssignments).not.toHaveBeenCalled(); + expect(mockFetchCourseEnrollments).toHaveBeenCalledTimes(1); + + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: true, + data: undefined, + }), + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + spentTransactions: { + count: 1, + results: [{ id: 'mock-course-enrollment-id' }], + }, + }, + }), + ); + }); + + it.each([ + { hasAssignableBudget: false, isTopDownAssignmentEnabled: false }, + { hasAssignableBudget: true, isTopDownAssignmentEnabled: false }, + { hasAssignableBudget: false, isTopDownAssignmentEnabled: true }, + { hasAssignableBudget: true, isTopDownAssignmentEnabled: true }, + ])('handles when budget is a subsidy access policy uuid (not an enterprise offer id) (%s)', async ({ hasAssignableBudget, isTopDownAssignmentEnabled }) => { + useBudgetId.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + subsidyAccessPolicyId: mockSubsidyAccessPolicyUUID, + }); + useSubsidyAccessPolicy.mockReturnValue({ + data: hasAssignableBudget ? mockAssignableSubsidyAccessPolicy : mockPerLearnerSpendLimitSubsidyAccessPolicy, + }); + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + if (hasAssignableBudget && isTopDownAssignmentEnabled) { + mockListContentAssignments.mockResolvedValue({ + data: { + count: 1, + results: [{ id: 'mock-content-assignment-id' }], + }, + }); + } + const mockFetchCourseEnrollments = jest.spyOn(EnterpriseDataApiService, 'fetchCourseEnrollments'); + mockFetchCourseEnrollments.mockResolvedValue({ + data: { + count: 1, + results: [{ id: 'mock-course-enrollment-id' }], + }, + }); + + const { result, waitForNextUpdate } = renderHook( + () => useBudgetDetailActivityOverview({ + enterpriseUUID: mockEnterpriseUUID, + isTopDownAssignmentEnabled: true, + }), + { wrapper }, + ); + + expect(useSubsidyAccessPolicy).toHaveBeenCalledWith(mockSubsidyAccessPolicyUUID); + + expect(mockFetchCourseEnrollments).toHaveBeenCalledTimes(1); + if (hasAssignableBudget) { + expect(mockListContentAssignments).toHaveBeenCalledTimes(1); + } else { + expect(mockListContentAssignments).not.toHaveBeenCalled(); + } + + await waitForNextUpdate(); + + const expectedData = { + spentTransactions: { + count: 1, + results: [{ id: 'mock-course-enrollment-id' }], + }, + }; + + if (hasAssignableBudget && isTopDownAssignmentEnabled) { + expectedData.contentAssignments = { + count: 1, + results: [{ id: 'mock-content-assignment-id' }], + }; + } + + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: expectedData, + }), + ); + }); +}); diff --git a/src/components/learner-credit-management/data/hooks/useIsLargeOrGreater.js b/src/components/learner-credit-management/data/hooks/useIsLargeOrGreater.js new file mode 100644 index 0000000000..b3ac4f0a64 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useIsLargeOrGreater.js @@ -0,0 +1,5 @@ +import { breakpoints, useMediaQuery } from '@edx/paragon'; + +const useIsLargeOrGreater = () => useMediaQuery({ query: `(min-width: ${breakpoints.large.minWidth}px)` }); + +export default useIsLargeOrGreater; diff --git a/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js b/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js new file mode 100644 index 0000000000..1ff9faf586 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js @@ -0,0 +1,12 @@ +import { useRouteMatch, generatePath } from 'react-router-dom'; + +import useBudgetId from './useBudgetId'; + +const usePathToCatalogTab = () => { + const { budgetId } = useBudgetId(); + const routeMatch = useRouteMatch(); + const pathToCatalogTab = generatePath(routeMatch.path, { budgetId, activeTabKey: 'catalog' }); + return pathToCatalogTab; +}; + +export default usePathToCatalogTab; diff --git a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js index c36d72ffa0..7058cdcb1a 100644 --- a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js +++ b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js @@ -2,10 +2,13 @@ import { useQuery } from '@tanstack/react-query'; import { camelCaseObject } from '@edx/frontend-platform/utils'; import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import { learnerCreditManagementQueryKeys } from '../constants'; -const determineBudgetAssignability = (policyType) => { +const determineBudgetAssignability = (subsidyAccessPolicy) => { + const policyType = subsidyAccessPolicy?.policyType; + const isAssignable = !!subsidyAccessPolicy?.assignmentConfiguration; const assignableSubsidyAccessPolicyTypes = ['AssignedLearnerCreditAccessPolicy']; - return assignableSubsidyAccessPolicyTypes.includes(policyType); + return isAssignable && assignableSubsidyAccessPolicyTypes.includes(policyType); }; /** @@ -18,12 +21,12 @@ const getSubsidyAccessPolicy = async ({ queryKey }) => { const subsidyAccessPolicyUUID = queryKey[2]; const response = await EnterpriseAccessApiService.retrieveSubsidyAccessPolicy(subsidyAccessPolicyUUID); const subsidyAccessPolicy = camelCaseObject(response.data); - subsidyAccessPolicy.isAssignable = determineBudgetAssignability(subsidyAccessPolicy.policyType); + subsidyAccessPolicy.isAssignable = determineBudgetAssignability(subsidyAccessPolicy); return subsidyAccessPolicy; }; const useSubsidyAccessPolicy = (subsidyAccessPolicyId, { queryOptions } = {}) => useQuery({ - queryKey: ['learner-credit-management', 'subsidy-access-policy', subsidyAccessPolicyId], + queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), queryFn: getSubsidyAccessPolicy, enabled: !!subsidyAccessPolicyId, ...queryOptions, diff --git a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx index 4d67e12051..30edc0f3cc 100644 --- a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx @@ -5,6 +5,7 @@ import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; // Import the hoo import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; const mockSubsidyAccessPolicyUUID = '9af340a9-48de-4d94-976d-e2282b9eb7f3'; +const mockAssignmentConfiguration = { uuid: 'test-assignment-configuration-uuid' }; // Mock the EnterpriseAccessApiService jest.mock('../../../../data/services/EnterpriseAccessApiService', () => ({ @@ -43,6 +44,7 @@ describe('useSubsidyAccessPolicy', () => { data: { uuid: mockSubsidyAccessPolicyUUID, policyType: isAssignable ? 'AssignedLearnerCreditAccessPolicy' : 'PerLearnerCreditSpendLimitAccessPolicy', + assignmentConfiguration: isAssignable ? mockAssignmentConfiguration : undefined, // Other properties... }, }); @@ -59,6 +61,7 @@ describe('useSubsidyAccessPolicy', () => { uuid: mockSubsidyAccessPolicyUUID, policyType: isAssignable ? 'AssignedLearnerCreditAccessPolicy' : 'PerLearnerCreditSpendLimitAccessPolicy', isAssignable, + assignmentConfiguration: isAssignable ? mockAssignmentConfiguration : undefined, // Other expected properties... }); }); @@ -89,6 +92,7 @@ describe('useSubsidyAccessPolicy', () => { expectedData: { uuid: mockSubsidyAccessPolicyUUID, policyType: 'AssignedLearnerCreditAccessPolicy', + assignmentConfiguration: mockAssignmentConfiguration, isAssignable: true, // Other expected properties... }, @@ -102,6 +106,7 @@ describe('useSubsidyAccessPolicy', () => { data: { uuid: mockSubsidyAccessPolicyUUID, policyType: 'AssignedLearnerCreditAccessPolicy', + assignmentConfiguration: mockAssignmentConfiguration, // Other properties... }, }); diff --git a/src/components/learner-credit-management/data/tests/constants.js b/src/components/learner-credit-management/data/tests/constants.js new file mode 100644 index 0000000000..a72d643f63 --- /dev/null +++ b/src/components/learner-credit-management/data/tests/constants.js @@ -0,0 +1,25 @@ +export const mockEnterpriseOfferId = '123'; +export const mockSubsidyAccessPolicyUUID = 'c17de32e-b80b-468f-b994-85e68fd32751'; + +export const mockAssignableSubsidyAccessPolicy = { + uuid: mockSubsidyAccessPolicyUUID, + policyType: 'AssignedLearnerCreditAccessPolicy', + assignmentConfiguration: { + uuid: 'test-uuid', + }, + displayName: 'Assignable Learner Credit', + aggregates: { + spendAvailableUsd: 10000, + }, + isAssignable: true, +}; + +export const mockPerLearnerSpendLimitSubsidyAccessPolicy = { + uuid: mockSubsidyAccessPolicyUUID, + policyType: 'PerLearnerSpendCreditAccessPolicy', + displayName: 'Per Learner Spend Limit', + aggregates: { + spendAvailableUsd: 10000, + }, + isAssignable: false, +}; diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 681c421759..9874c22f4b 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -1,11 +1,15 @@ import { v4 as uuidv4 } from 'uuid'; import dayjs from 'dayjs'; +import { camelCaseObject } from '@edx/frontend-platform'; import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, } from './constants'; import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; +import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; +import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiService'; + /** * Transforms offer summary from API for display in the UI, guarding * against bad data (e.g., accounting for refunds). @@ -147,6 +151,7 @@ export const formatPrice = (price, options = {}) => { const USDollar = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', + minimumFractionDigits: 0, ...options, }); return USDollar.format(Math.abs(price)); @@ -185,6 +190,101 @@ export const orderOffers = (offers) => { return offers; }; +/** + * Formats a date string to MMM D, YYYY format. + * @param {string} date Date string. + * @returns Formatted date string. + */ export function formatDate(date) { return dayjs(date).format('MMM D, YYYY'); } + +/** + * Retrieves content assignments for the given budget's assignment configuration UUID (retrieved from the associated + * subsidy access policy). + * + * @param {String} assignmentConfigurationUUID The UUID of the assignment configuration. + * @param {Object} options Optional options object to pass/override query parameters. + * + * @returns Camelcased response from the content assignments. + */ +export async function fetchContentAssignments(assignmentConfigurationUUID, options = {}) { + const response = await EnterpriseAccessApiService.listContentAssignments(assignmentConfigurationUUID, options); + return camelCaseObject(response.data); +} + +/** + * Retrieves spent transactions for the given budget (either a subsidy access + * policy or an enterprise offer), if any. + * + * @param {Object} args An object containing various arguments. + * @param {String} args.enterpriseUUID The UUID of the enterprise customer. + * @param {String} args.subsidyAccessPolicyId The UUID of a subsidy access policy, if any. + * @param {String} args.enterpriseOfferId The UUID of an enterprise offer, if any. + * + * @returns Camelcased response from the spent transactions. + */ +export async function fetchSpentTransactions({ + enterpriseUUID, + subsidyAccessPolicyId, + enterpriseOfferId, +}) { + const options = { + page: 1, + pageSize: 25, + ignoreNullCourseListPrice: true, + }; + + if (subsidyAccessPolicyId) { + options.budgetId = subsidyAccessPolicyId; + } else if (enterpriseOfferId) { + options.offerId = enterpriseOfferId; + } + + const response = await EnterpriseDataApiService.fetchCourseEnrollments( + enterpriseUUID, + options, + ); + return camelCaseObject(response.data); +} + +/** + * Retrieves the requisite overview budget detail activity from relevant APIs, including spent transactions + * and (if applicable) any content assignments for the budget. Content assignments are only fetched when the + * budget is a subsidy access policy that is assignable and the top-down assignment feature is enabled. + * + * @param {Object} args An object containing various arguments. Note: `@tanstack/reat-query` passes + * additional arguments. + * @param {Array} args.budgetId The budget id for the currently viewing budget. + * @param {Object} [args.subsidyAccessPolicy] The subsidy access policy metadata, if any. Not + * applicable when the budget is an enterprise offer. + * @param {String} args.enterpriseUUID The UUID of the enterprise customer. + * @param {Boolean} args.isTopDownAssignmentEnabled Whether the top-down assignment feature is enabled. + * @returns An object containing the first page of spent transactions and (if applicable) content assignments. + */ +export async function retrieveBudgetDetailActivityOverview({ + budgetId, + subsidyAccessPolicy, + enterpriseUUID, + isTopDownAssignmentEnabled, +}) { + const isBudgetAssignable = !!(isTopDownAssignmentEnabled && subsidyAccessPolicy?.isAssignable); + const promisesToFulfill = [ + fetchSpentTransactions({ + enterpriseUUID, + subsidyAccessPolicyId: subsidyAccessPolicy?.uuid, + enterpriseOfferId: budgetId, + }), + ]; + if (isBudgetAssignable) { + promisesToFulfill.push(fetchContentAssignments(subsidyAccessPolicy.assignmentConfiguration.uuid)); + } + const responses = await Promise.allSettled(promisesToFulfill); + const result = { + spentTransactions: responses[0].value, + }; + if (isBudgetAssignable) { + result.contentAssignments = responses[1].value; + } + return result; +} diff --git a/src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss b/src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss new file mode 100644 index 0000000000..8a1e4db6e7 --- /dev/null +++ b/src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss @@ -0,0 +1,14 @@ +// The `Card` component in Paragon does not seem to properly let consumers customize the width of the `Card.Body` +// contents when in the horizontal card orientation without custom CSS. As a result, both the `Card.Footer` and +// `Card.Body` incorrectly get equal column widths when the preference is that the `Card.Body` has more width than +// the `Card.Footer`. The below styles force the `Card.Body` to have appropriately more width than the `Card.Footer` when +// the `Card` is in the horizontal orientation. + +.assign-more-courses-empty-state-minimal { + .assign-more-courses__card-body { + flex: 3; + } + .assign-more-courses__card-footer { + flex: 1; + } +} diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 1e32550883..96280e7e68 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -16,15 +16,20 @@ import { useSubsidyAccessPolicy, useOfferRedemptions, useBudgetContentAssignments, + useBudgetDetailActivityOverview, + useIsLargeOrGreater, } from '../data'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; +import { + mockAssignableSubsidyAccessPolicy, + mockPerLearnerSpendLimitSubsidyAccessPolicy, + mockSubsidyAccessPolicyUUID, + mockEnterpriseOfferId, +} from '../data/tests/constants'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useParams: jest.fn().mockReturnValue({ - budgetId: '123', - activeTabKey: 'activity', - }), + useParams: jest.fn(), })); jest.mock('../data', () => ({ @@ -32,36 +37,10 @@ jest.mock('../data', () => ({ useOfferRedemptions: jest.fn(), useBudgetContentAssignments: jest.fn(), useSubsidyAccessPolicy: jest.fn(), + useBudgetDetailActivityOverview: jest.fn(), + useIsLargeOrGreater: jest.fn().mockReturnValue(true), })); -useSubsidyAccessPolicy.mockReturnValue({ - isInitialLoading: false, - data: { - uuid: 'test-budget-uuid', - policyType: 'PerLearnerSpendCreditAccessPolicy', - displayName: null, - isAssignable: false, - }, -}); -useBudgetContentAssignments.mockReturnValue({ - isLoading: false, - contentAssignments: { - count: 0, - results: [], - numPages: 1, - }, - fetchContentAssignments: jest.fn(), -}); -useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: { - itemCount: 0, - pageCount: 0, - results: [], - }, - fetchOfferRedemptions: jest.fn(), -}); - const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); const enterpriseSlug = 'test-enterprise'; @@ -77,9 +56,18 @@ const initialStoreState = { }, }; -const mockEnterpriseOfferId = '123'; -const mockSubsidyAccessPolicyUUID = 'c17de32e-b80b-468f-b994-85e68fd32751'; - +const mockLearnerEmail = 'edx@example.com'; +const mockCourseKey = 'edX+DemoX'; +const mockContentTitle = 'edx Demo'; +const mockEmptyStateBudgetDetailActivityOverview = { + contentAssignments: { count: 0 }, + spentTransactions: { count: 0 }, +}; +const mockEmptyOfferRedemptions = { + itemCount: 0, + pageCount: 0, + results: [], +}; const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; @@ -107,27 +95,24 @@ const BudgetDetailPageWrapper = ({ describe('', () => { beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(() => { - useParams.mockReturnValue({ - budgetId: '123', - activeTabKey: 'activity', - }); + jest.resetAllMocks(); }); it.each([ { displayName: null }, { displayName: 'Test Budget Display Name' }, - ])('renders budget header data', ({ displayName }) => { + ])('renders budget header data (%s)', ({ displayName }) => { + useParams.mockReturnValue({ + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + activeTabKey: 'activity', + }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: false, - data: { - uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', - policyType: 'AssignedLearnerCreditAccessPolicy', - displayName, - }, + data: { ...mockAssignableSubsidyAccessPolicy, displayName }, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: mockEmptyStateBudgetDetailActivityOverview, }); const expectedDisplayName = displayName || 'Overview'; renderWithRouter(); @@ -140,6 +125,32 @@ describe('', () => { expect(screen.getByText(expectedDisplayName, { selector: 'h2' })); }); + it.each([ + { isLargeViewport: true }, + { isLargeViewport: false }, + ])('displays budget activity overview empty state', ({ isLargeViewport }) => { + useIsLargeOrGreater.mockReturnValue(isLargeViewport); + useParams.mockReturnValue({ + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: mockEmptyStateBudgetDetailActivityOverview, + }); + renderWithRouter(); + + // Overview empty state (no content assignments, no spent transactions) + expect(screen.getByText('No budget activity yet? Assign a course!')).toBeInTheDocument(); + const illustrationTestIds = ['find-the-right-course-illustration', 'name-your-learners-illustration', 'confirm-spend-illustration']; + illustrationTestIds.forEach(testId => expect(screen.getByTestId(testId)).toBeInTheDocument()); + expect(screen.getByText('Get started', { selector: 'a' })).toBeInTheDocument(); + }); + it.each([ { budgetId: mockEnterpriseOfferId, @@ -157,13 +168,29 @@ describe('', () => { budgetId, activeTabKey: 'activity', }); - useOfferRedemptions.mockReturnValue({ + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: undefined, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: undefined, + spentTransactions: { count: 1 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ isLoading: false, - offerRedemptions: { - itemCount: 0, - pageCount: 0, + contentAssignments: { + count: 0, results: [], + numPages: 1, }, + fetchContentAssignments: jest.fn(), + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, fetchOfferRedemptions: jest.fn(), }); renderWithRouter(); @@ -181,41 +208,63 @@ describe('', () => { expect(spentSection.getByText('No results found')).toBeInTheDocument(); }); - it('renders with empty assigned table and catalog tab available for assignable budgets', () => { + it('renders with assigned table empty state with spent table and catalog tab available for assignable budgets', () => { useParams.mockReturnValue({ - budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, data: { - uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', - policyType: 'AssignedLearnerCreditAccessPolicy', - isAssignable: true, + contentAssignments: { count: 0 }, + spentTransactions: { count: 1 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 0, + results: [], + numPages: 1, }, + fetchContentAssignments: jest.fn(), + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), }); renderWithRouter(); - // Assigned table is visible within Activity tab contents - const assignedSection = within(screen.getByText('Assigned').closest('section')); - expect(assignedSection.getByText('No results found')).toBeInTheDocument(); + // Assigned table empty state is visible within Activity tab contents + expect(screen.getByText('Assign more courses to maximize your budget.')).toBeInTheDocument(); + expect(screen.getByText('available balance of $10,000', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('Assign courses', { selector: 'a' })).toBeInTheDocument(); // Catalog tab exists and is NOT active expect(screen.getByText('Catalog').getAttribute('aria-selected')).toBe('false'); }); it('renders with assigned table data', () => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, data: { - uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', - policyType: 'AssignedLearnerCreditAccessPolicy', - isAssignable: true, + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, }, }); - const mockLearnerEmail = 'edx@example.com'; - const mockContentTitle = 'edx Demo'; - const mockCourseKey = 'edX+DemoX'; useBudgetContentAssignments.mockReturnValue({ isLoading: false, contentAssignments: { @@ -232,28 +281,38 @@ describe('', () => { currentPage: 1, }, }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); renderWithRouter(); // Assigned table is visible within Activity tab contents const assignedSection = within(screen.getByText('Assigned').closest('section')); expect(assignedSection.queryByText('No results found')).not.toBeInTheDocument(); expect(assignedSection.getByText(mockLearnerEmail)).toBeInTheDocument(); - const viewCourseCTA = assignedSection.getByText('edx Demo', { selector: 'a' }); + const viewCourseCTA = assignedSection.getByText(mockContentTitle, { selector: 'a' }); expect(viewCourseCTA).toBeInTheDocument(); expect(viewCourseCTA.getAttribute('href')).toEqual(`${process.env.ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${mockCourseKey}`); }); it('renders with assigned table data "View Course" hyperlink default when content title is null', () => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, data: { - uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', - policyType: 'AssignedLearnerCreditAccessPolicy', - isAssignable: true, + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, }, }); - const mockLearnerEmail = 'edx@example.com'; - const mockCourseKey = 'edX+DemoX'; useBudgetContentAssignments.mockReturnValue({ isLoading: false, contentAssignments: { @@ -268,6 +327,12 @@ describe('', () => { numPages: 1, currentPage: 1, }, + fetchContentAssignments: jest.fn(), + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), }); renderWithRouter(); @@ -282,9 +347,17 @@ describe('', () => { it('renders with catalog tab active on initial load for assignable budgets', async () => { useParams.mockReturnValue({ - budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'catalog', }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValueOnce({ + isLoading: false, + data: mockEmptyStateBudgetDetailActivityOverview, + }); renderWithRouter(); // Catalog tab exists and is active @@ -292,12 +365,19 @@ describe('', () => { }); it('hides catalog tab when budget is not assignable', () => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: false, + data: mockPerLearnerSpendLimitSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, data: { - uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', - policyType: 'PerLearnerSpendCreditAccessPolicy', - isAssignable: false, + contentAssignments: undefined, + spentTransactions: { count: 0 }, }, }); renderWithRouter(); @@ -306,7 +386,7 @@ describe('', () => { expect(screen.queryByText('Catalog')).toBeFalsy(); }); - it('hides catalog tab when enterpriseFeatures.topDownAssignmentRealTimeLcm', () => { + it('hides catalog tab when enterpriseFeatures.topDownAssignmentRealTimeLcm is disabled', () => { const initialState = { portalConfiguration: { ...initialStoreState.portalConfiguration, @@ -315,6 +395,21 @@ describe('', () => { }, }, }; + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: undefined, + spentTransactions: { count: 0 }, + }, + }); renderWithRouter(); // Catalog tab does NOT exist @@ -323,9 +418,20 @@ describe('', () => { it('defaults to activity tab is no activeTabKey is provided', () => { useParams.mockReturnValue({ - budgetId: '123', + budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: undefined, }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: undefined, + spentTransactions: { count: 0 }, + }, + }); renderWithRouter(); // Activity tab exists and is active @@ -334,22 +440,34 @@ describe('', () => { it('displays not found message is invalid activeTabKey is provided', () => { useParams.mockReturnValue({ - budgetId: '123', + budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'invalid', }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: mockEmptyStateBudgetDetailActivityOverview, + }); renderWithRouter(); expect(screen.getByText('404')).toBeInTheDocument(); expect(screen.getByText('something went wrong', { exact: false })).toBeInTheDocument(); }); it('handles user switching to catalog tab', async () => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: false, - data: { - uuid: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', - policyType: 'AssignedLearnerCreditAccessPolicy', - isAssignable: true, - }, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: mockEmptyStateBudgetDetailActivityOverview, }); renderWithRouter(); const catalogTab = screen.getByText('Catalog'); @@ -364,11 +482,18 @@ describe('', () => { }); it('displays loading message while loading subsidy access policy metadata from API', () => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: true, data: undefined, }); - + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: mockEmptyStateBudgetDetailActivityOverview, + }); renderWithRouter( ', () => { expect(screen.getByText('loading budget details')).toBeInTheDocument(); }); + + it.each([ + { isActivityOverviewLoading: true }, + { isActivityOverviewLoading: false }, + ])('displays loading skeletons while fetching budget detail activity overview data from API endpoints (%s)', ({ isActivityOverviewLoading }) => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: isActivityOverviewLoading, + data: undefined, + }); + renderWithRouter(); + + expect(screen.getByText('loading budget activity overview')).toBeInTheDocument(); + }); }); diff --git a/src/index.scss b/src/index.scss index 34c7e99ba0..dfdae9bc8e 100644 --- a/src/index.scss +++ b/src/index.scss @@ -24,6 +24,7 @@ $modal-max-width: 650px; @import "./components/BulkEnrollmentPage/BulkEnrollment"; @import "./components/Admin/Admin"; @import "./components/settings/settings"; +@import "./components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal"; body { overflow-x: hidden; From c0a76057ecf79514e682d4180675acc62baa4efb Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 27 Oct 2023 14:42:22 -0400 Subject: [PATCH 047/124] fix: handle additional case when not viewing assignable budget (#1073) --- .../BudgetDetailActivityTabContents.jsx | 8 +++++++- .../tests/BudgetDetailPage.test.jsx | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 517c4e4954..123b48f7e5 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -5,11 +5,13 @@ import { Stack, Skeleton } from '@edx/paragon'; import BudgetDetailRedemptions from './BudgetDetailRedemptions'; import BudgetDetailAssignments from './BudgetDetailAssignments'; -import { useBudgetDetailActivityOverview } from './data'; +import { useBudgetDetailActivityOverview, useBudgetId, useSubsidyAccessPolicy } from './data'; import NoBudgetActivityEmptyState from './NoBudgetActivityEmptyState'; const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) => { const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm; + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const { isLoading: isBudgetActivityOverviewLoading, data: budgetActivityOverview, @@ -27,6 +29,10 @@ const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) ); } + if (!isTopDownAssignmentEnabled || !subsidyAccessPolicy?.isAssignable) { + return ; + } + const hasContentAssignments = !!budgetActivityOverview.contentAssignments?.count > 0; const hasSpentTransactions = !!budgetActivityOverview.spentTransactions?.count > 0; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 96280e7e68..fd87153c21 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -280,6 +280,7 @@ describe('', () => { numPages: 1, currentPage: 1, }, + fetchContentAssignments: jest.fn(), }); useOfferRedemptions.mockReturnValue({ isLoading: false, @@ -380,10 +381,18 @@ describe('', () => { spentTransactions: { count: 0 }, }, }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); renderWithRouter(); // Catalog tab does NOT exist expect(screen.queryByText('Catalog')).toBeFalsy(); + + // Ensure no assignments-related empty states are rendered + expect(screen.queryByText('No budget activity yet? Assign a course!')).not.toBeInTheDocument(); }); it('hides catalog tab when enterpriseFeatures.topDownAssignmentRealTimeLcm is disabled', () => { @@ -410,10 +419,18 @@ describe('', () => { spentTransactions: { count: 0 }, }, }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); renderWithRouter(); // Catalog tab does NOT exist expect(screen.queryByText('Catalog')).toBeFalsy(); + + // Ensure no assignments-related empty states are rendered + expect(screen.queryByText('No budget activity yet? Assign a course!')).not.toBeInTheDocument(); }); it('defaults to activity tab is no activeTabKey is provided', () => { From 017f3490bbb2f7dce34ca3e3341a6c4393ed25be Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:53:14 -0700 Subject: [PATCH 048/124] feat: display search result cards in catalog tab (#1059) * feat: display search result cards in catalog tab * fix: failing test in BudgetDetailPage * fix: replace word register with enroll * fix: implemented reviewer comments * fix: lint error * fix: lint error * feat: added policy's catalog uuid to search filter * fix: failing test * fix: refactored based on reviewer feedback * fix: lint error * fix: refactored code to include new api field and updated test * fix: removing unused prop in test * fix: refactored * fix: search filters * chore: refactored --- .../BudgetDetailCatalogTabContents.jsx | 14 +- .../cards/CourseCard.jsx | 152 +++++++++++------- .../cards/CourseCard.test.jsx | 71 ++++---- .../learner-credit-management/constants.js | 18 +++ .../data/constants.js | 8 +- .../learner-credit-management/data/utils.js | 11 ++ .../learner-credit-management/index.jsx | 1 - .../learner-credit.scss | 25 --- .../search/CatalogSearch.jsx | 14 +- .../search/CatalogSearchResults.jsx | 44 ++--- .../tests/CatalogSearchResults.test.jsx | 14 +- 11 files changed, 214 insertions(+), 158 deletions(-) create mode 100644 src/components/learner-credit-management/constants.js delete mode 100644 src/components/learner-credit-management/learner-credit.scss diff --git a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx index a99f268db3..a143c1f07a 100644 --- a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx @@ -5,10 +5,20 @@ import { Row, Col } from '@edx/paragon'; import { SearchData, SEARCH_FACET_FILTERS } from '@edx/frontend-enterprise-catalog-search'; import CatalogSearch from './search/CatalogSearch'; -import { LANGUAGE_REFINEMENT, LEARNING_TYPE_REFINEMENT } from './data'; +import { + LANGUAGE_REFINEMENT, + LEARNING_TYPE_REFINEMENT, + useBudgetId, + useSubsidyAccessPolicy, +} from './data'; import { configuration } from '../../config'; const BudgetDetailCatalogTabContents = () => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { + data: subsidyAccessPolicy, + } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const language = { attribute: LANGUAGE_REFINEMENT, title: 'Language', @@ -38,7 +48,7 @@ const BudgetDetailCatalogTabContents = () => { indexName={configuration.ALGOLIA.INDEX_NAME} searchClient={searchClient} > - + diff --git a/src/components/learner-credit-management/cards/CourseCard.jsx b/src/components/learner-credit-management/cards/CourseCard.jsx index 9284369239..f4885ce433 100644 --- a/src/components/learner-credit-management/cards/CourseCard.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.jsx @@ -3,95 +3,137 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { camelCaseObject } from '@edx/frontend-platform'; -import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; import { - Badge, Button, Card, Hyperlink, + Badge, + Button, + Card, + Stack, + Hyperlink, + useMediaQuery, + breakpoints, } from '@edx/paragon'; -import { EXEC_COURSE_TYPE } from '../data/constants'; -import { formatDate } from '../data/utils'; +import { injectIntl } from '@edx/frontend-platform/i18n'; +import { camelCaseObject } from '@edx/frontend-platform'; +import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; + +import { EXEC_ED_COURSE_TYPE } from '../data'; +import { formatPrice, formatDate, getEnrollmentDeadline } from '../data/utils'; +import CARD_TEXT from '../constants'; const CourseCard = ({ - onClick, original, + original, }) => { const { - title, + availability, cardImageUrl, courseType, normalizedMetadata, partners, + title, } = camelCaseObject(original); - let priceText; + const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); + const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); + + const { + BADGE, + BUTTON_ACTION, + PRICE, + ENROLLMENT, + } = CARD_TEXT; + + const price = normalizedMetadata?.contentPrice ? formatPrice(normalizedMetadata.contentPrice, { minimumFractionDigits: 0 }) : 'N/A'; + + const imageSrc = cardImageUrl || cardFallbackImg; + + let logoSrc; + let logoAlt; + if (partners.length === 1) { + logoSrc = partners[0]?.logoImageUrl; + logoAlt = `${partners[0]?.name}'s logo`; + } + const altText = `${title} course image`; + const formattedAvailability = availability?.length ? availability.join(', ') : null; + + const enrollmentDeadline = getEnrollmentDeadline(normalizedMetadata?.enrollByDate); + + let courseEnrollmentInfo; + let execEdEnrollmentInfo; + if (normalizedMetadata?.enrollByDate) { + courseEnrollmentInfo = `${formattedAvailability} • ${ENROLLMENT.text} ${enrollmentDeadline}`; + execEdEnrollmentInfo = `Starts ${formatDate(normalizedMetadata.startDate)} • + ${ENROLLMENT.text} ${enrollmentDeadline}`; + } else { + courseEnrollmentInfo = formattedAvailability; + execEdEnrollmentInfo = formattedAvailability; + } + + const isExecEd = courseType === EXEC_ED_COURSE_TYPE; + return ( onClick(original)} - orientation="horizontal" - tabIndex="0" + orientation={isSmall ? 'vertical' : 'horizontal'} > -
-
-

{title}

-

{partners[0]?.name}

- {courseType === EXEC_COURSE_TYPE && ( - - Executive Education - - )} - {courseType !== EXEC_COURSE_TYPE && ( -

+ + +

{price}

+ {PRICE.subText} + )} -

- Starts {formatDate(normalizedMetadata?.start_date)} • - Learner must register by {formatDate(normalizedMetadata?.enroll_by_date)} -

-
- -

{priceText}

-

Per learner price

- - - - - + /> + + + {isExecEd ? BADGE.execEd : BADGE.course} + -
+ + + + +
); }; -CourseCard.defaultProps = { - onClick: () => {}, -}; - CourseCard.propTypes = { - onClick: PropTypes.func, original: PropTypes.shape({ - title: PropTypes.string, + availability: PropTypes.arrayOf(PropTypes.string), cardImageUrl: PropTypes.string, + courseType: PropTypes.string, + normalizedMetadata: PropTypes.shape(), + originalImageUrl: PropTypes.string, partners: PropTypes.arrayOf( PropTypes.shape({ + logoImageUrl: PropTypes.string, name: PropTypes.string, - logo_image_url: PropTypes.string, }), ), - normalizedMetadata: PropTypes.shape({ - startDate: PropTypes.string, - endDate: PropTypes.string, - enrollByDate: PropTypes.string, - }), - courseType: PropTypes.string, + title: PropTypes.string, }).isRequired, }; -export default CourseCard; +export default injectIntl(CourseCard); diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/CourseCard.test.jsx index 963137e178..704a1f825c 100644 --- a/src/components/learner-credit-management/cards/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.test.jsx @@ -4,47 +4,46 @@ import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import CourseCard from './CourseCard'; -import { CONTENT_TYPE_COURSE, EXEC_ED_TITLE } from '../data/constants'; - -jest.mock('@edx/frontend-platform', () => ({ - ...jest.requireActual('@edx/frontend-platform'), -})); - -const TEST_CATALOG = ['ayylmao']; const originalData = { - title: 'Course Title', + availability: ['Upcoming'], card_image_url: undefined, - partners: [{ logo_image_url: '', name: 'Course Provider' }], - first_enrollable_paid_seat_price: 100, + course_type: 'course', + normalized_metadata: { + enroll_by_date: '2016-02-18T04:00:00Z', + start_date: '2016-04-18T04:00:00Z', + content_price: 100, + }, original_image_url: '', - enterprise_catalog_query_titles: TEST_CATALOG, - advertised_course_run: { pacing_type: 'self_paced' }, + partners: [{ logo_image_url: '', name: 'Course Provider' }], + title: 'Course Title', }; const defaultProps = { original: originalData, - learningType: CONTENT_TYPE_COURSE, }; const execEdData = { - title: 'Exec Ed Course Title', + availability: ['Upcoming'], card_image_url: undefined, - partners: [{ logo_image_url: '', name: 'Course Provider' }], - first_enrollable_paid_seat_price: 100, - original_image_url: '', - enterprise_catalog_query_titles: TEST_CATALOG, - advertised_course_run: { pacing_type: 'instructor_paced' }, + course_type: 'executive-education-2u', entitlements: [{ price: '999.00' }], + normalized_metadata: { + enroll_by_date: '2016-02-18T04:00:00Z', + start_date: '2016-04-18T04:00:00Z', + content_price: 999, + }, + original_image_url: '', + partners: [{ logo_image_url: '', name: 'Course Provider' }], + title: 'Exec Ed Title', }; const execEdProps = { original: execEdData, - learningType: EXEC_ED_TITLE, }; describe('Course card works as expected', () => { - test('card renders as expected', () => { + test('course card renders', () => { render( @@ -54,21 +53,14 @@ describe('Course card works as expected', () => { expect( screen.queryByText(defaultProps.original.partners[0].name), ).toBeInTheDocument(); - expect(screen.queryByText('Course Title')).toBeInTheDocument(); + expect(screen.queryByText('$100')).toBeInTheDocument(); expect(screen.queryByText('Per learner price')).toBeInTheDocument(); + expect(screen.queryByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); + expect(screen.queryByText('Course')).toBeInTheDocument(); + expect(screen.queryByText('View course')).toBeInTheDocument(); + expect(screen.queryByText('Assign')).toBeInTheDocument(); }); - test('exec ed card renders as expected', () => { - render( - - - , - ); - expect(screen.queryByText(execEdProps.original.title)).toBeInTheDocument(); - expect( - screen.queryByText(execEdProps.original.partners[0].name), - ).toBeInTheDocument(); - expect(screen.queryByText('Exec Ed Course Title')).toBeInTheDocument(); - }); + test('test card renders default image', async () => { render( @@ -79,4 +71,15 @@ describe('Course card works as expected', () => { fireEvent.error(screen.getByAltText(imageAltText)); await expect(screen.getByAltText(imageAltText).src).not.toBeUndefined; }); + + test('exec ed card renders', async () => { + render( + + + , + ); + expect(screen.queryByText('$999')).toBeInTheDocument(); + expect(screen.queryByText('Starts Apr 18, 2016 • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).toBeInTheDocument(); + }); }); diff --git a/src/components/learner-credit-management/constants.js b/src/components/learner-credit-management/constants.js new file mode 100644 index 0000000000..7148a852d2 --- /dev/null +++ b/src/components/learner-credit-management/constants.js @@ -0,0 +1,18 @@ +const CARD_TEXT = { + BADGE: { + course: 'Course', + execEd: 'Executive Education', + }, + BUTTON_ACTION: { + viewCourse: 'View course', + assign: 'Assign', + }, + ENROLLMENT: { + text: 'Learner must enroll by', + }, + PRICE: { + subText: 'Per learner price', + }, +}; + +export default CARD_TEXT; diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 8a93ee6f6d..08ad16ef5d 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -32,12 +32,18 @@ export const LANGUAGE_REFINEMENT = 'language'; // Learning types export const CONTENT_TYPE_COURSE = 'course'; export const EXEC_ED_TITLE = 'Executive Education'; -export const EXEC_COURSE_TYPE = 'executive-education-2u'; +export const EXEC_ED_COURSE_TYPE = 'executive-education-2u'; + +// Learner must enroll within 90 days of assignment +export const ASSIGNMENT_ENROLLMENT_DEADLINE = 90; // Number of items to display per page in Budget Detail assignment/spend tables export const PAGE_SIZE = 25; export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array +// Number of items to display per page in Budget Catalog tab +export const SEARCH_RESULT_PAGE_SIZE = 15; + // Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`. // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const learnerCreditManagementQueryKeys = { diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 9874c22f4b..6a5a45888c 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -5,6 +5,7 @@ import { camelCaseObject } from '@edx/frontend-platform'; import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, + ASSIGNMENT_ENROLLMENT_DEADLINE, } from './constants'; import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; @@ -199,6 +200,16 @@ export function formatDate(date) { return dayjs(date).format('MMM D, YYYY'); } +// Exec ed and open courses cards should display either the enrollment deadline +// or 90 days from the present date on user pageload, whichever is sooner. +export function getEnrollmentDeadline(enrollByDate) { + const courseEnrollByDate = dayjs(enrollByDate); + const assignmentEnrollmentDeadline = dayjs().add(ASSIGNMENT_ENROLLMENT_DEADLINE, 'days'); + + return courseEnrollByDate <= assignmentEnrollmentDeadline + ? formatDate(courseEnrollByDate) + : formatDate(assignmentEnrollmentDeadline); +} /** * Retrieves content assignments for the given budget's assignment configuration UUID (retrieved from the associated * subsidy access policy). diff --git a/src/components/learner-credit-management/index.jsx b/src/components/learner-credit-management/index.jsx index eb68ef0a21..43786e8a0b 100644 --- a/src/components/learner-credit-management/index.jsx +++ b/src/components/learner-credit-management/index.jsx @@ -2,7 +2,6 @@ import React from 'react'; import { Route } from 'react-router-dom'; import PropTypes from 'prop-types'; import MultipleBudgetsPage from './MultipleBudgetsPage'; -import './learner-credit.scss'; import BudgetDetailPage from './BudgetDetailPage'; const LearnerCreditManagementRoutes = ({ match }) => ( diff --git a/src/components/learner-credit-management/learner-credit.scss b/src/components/learner-credit-management/learner-credit.scss deleted file mode 100644 index c26c3e9859..0000000000 --- a/src/components/learner-credit-management/learner-credit.scss +++ /dev/null @@ -1,25 +0,0 @@ -.card-container { - display: flex; - padding: 1rem; - flex-grow: 1; - justify-content: space-between; - - .section-1 { - flex-direction: column; - } - .section-2 { - margin-left: 0; - text-align: end !important; - min-width: 400px; - padding-right: 0; - justify-content: space-between; - .footer { - justify-content: end; - padding: 0; - } - } -} - -.badge { - margin: 4px; -} diff --git a/src/components/learner-credit-management/search/CatalogSearch.jsx b/src/components/learner-credit-management/search/CatalogSearch.jsx index c19d87dc50..d32fa3015a 100644 --- a/src/components/learner-credit-management/search/CatalogSearch.jsx +++ b/src/components/learner-credit-management/search/CatalogSearch.jsx @@ -1,19 +1,18 @@ import React from 'react'; -import { useParams } from 'react-router-dom'; import algoliasearch from 'algoliasearch/lite'; import { Configure, InstantSearch } from 'react-instantsearch-dom'; +import PropTypes from 'prop-types'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { SearchHeader } from '@edx/frontend-enterprise-catalog-search'; import { configuration } from '../../../config'; import CatalogSearchResults from './CatalogSearchResults'; +import { SEARCH_RESULT_PAGE_SIZE } from '../data'; -const CatalogSearch = () => { - const { budgetId } = useParams(); +const CatalogSearch = ({ catalogUuid }) => { const searchClient = algoliasearch(configuration.ALGOLIA.APP_ID, configuration.ALGOLIA.SEARCH_API_KEY); - - const searchFilters = `enterprise_catalog_query_uuids:${budgetId}`; + const searchFilters = `enterprise_catalog_uuids:${catalogUuid} AND content_type:course`; return (
@@ -28,6 +27,7 @@ const CatalogSearch = () => { { ); }; +CatalogSearch.propTypes = { + catalogUuid: PropTypes.string.isRequired, +}; + export default CatalogSearch; diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx index 5127568653..44faafff34 100644 --- a/src/components/learner-credit-management/search/CatalogSearchResults.jsx +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -1,19 +1,18 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useContext, useEffect, useMemo } from 'react'; import { connectStateResults } from 'react-instantsearch-dom'; import PropTypes from 'prop-types'; -import { SearchPagination } from '@edx/frontend-enterprise-catalog-search'; +import { SearchPagination, SearchContext } from '@edx/frontend-enterprise-catalog-search'; import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n'; import { - Alert, CardView, DataTable, Skeleton, + Alert, CardView, DataTable, TextFilter, } from '@edx/paragon'; import CourseCard from '../cards/CourseCard'; +import { SEARCH_RESULT_PAGE_SIZE } from '../data'; export const ERROR_MESSAGE = 'An error occurred while retrieving data'; -export const SKELETON_DATA_TESTID = 'enterprise-catalog-skeleton'; - /** * The core search results rendering component. * @@ -27,8 +26,10 @@ export const SKELETON_DATA_TESTID = 'enterprise-catalog-skeleton'; export const BaseCatalogSearchResults = ({ searchResults, + searchState, // algolia recommends this prop instead of searching isSearchStalled, + paginationComponent: PaginationComponent, error, setNoContent, }) => { @@ -58,20 +59,13 @@ export const BaseCatalogSearchResults = ({ () => searchResults?.hits || [], [searchResults?.hits], ); - - const renderCardComponent = (props) => ; + const { refinements } = useContext(SearchContext); + const page = refinements.page || (searchState.page || 0); useEffect(() => { setNoContent(searchResults === null || searchResults?.nbHits === 0); }, [searchResults, setNoContent]); - if (isSearchStalled) { - return ( -
- -
- ); - } if (error) { return ( @@ -88,21 +82,29 @@ export const BaseCatalogSearchResults = ({ return (
renderCardComponent(props)} + CardComponent={CourseCard} /> - + + +
); @@ -112,7 +114,6 @@ BaseCatalogSearchResults.defaultProps = { searchResults: { disjunctiveFacetsRefinements: [], nbHits: 0, hits: [] }, error: null, paginationComponent: SearchPagination, - preview: false, setNoContent: () => {}, }; @@ -137,7 +138,6 @@ BaseCatalogSearchResults.propTypes = { page: PropTypes.number, }).isRequired, paginationComponent: PropTypes.func, - preview: PropTypes.bool, setNoContent: PropTypes.func, }; diff --git a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx index 34a75e3ac0..3cbd296449 100644 --- a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx @@ -5,9 +5,7 @@ import '@testing-library/jest-dom/extend-expect'; import { SearchContext } from '@edx/frontend-enterprise-catalog-search'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { BaseCatalogSearchResults, SKELETON_DATA_TESTID } from '../search/CatalogSearchResults'; - -import { renderWithRouter } from '../../test/testUtils'; +import { BaseCatalogSearchResults } from '../search/CatalogSearchResults'; import { CONTENT_TYPE_COURSE } from '../data/constants'; @@ -141,14 +139,4 @@ describe('Main Catalogs view works as expected', () => { expect(screen.queryByText(TEST_COURSE_NAME_2)).toBeInTheDocument(); expect(screen.getAllByText('Showing 2 of 2.')[0]).toBeInTheDocument(); }); - test('isSearchStalled leads to rendering skeleton and not content', () => { - renderWithRouter( - - - , - ); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - expect(screen.queryByText(TEST_COURSE_NAME)).not.toBeInTheDocument(); - expect(screen.getByTestId(SKELETON_DATA_TESTID)).toBeInTheDocument(); - }); }); From 7ef63fec337dde2801cd65496ae0efef61064411 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Mon, 30 Oct 2023 11:16:49 -0400 Subject: [PATCH 049/124] chore: remove unused LCM components and one unused NPM package (#1069) --- package-lock.json | 13 -- package.json | 1 - src/components/TextAreaAutoSize/index.jsx | 1 + .../BudgetCard-V2.jsx | 80 ------- .../learner-credit-management/BudgetCard.jsx | 195 ++++------------ .../LearnerCreditManagement.jsx | 121 ---------- .../MultipleBudgetsPicker.jsx | 2 +- .../OfferNameHeading.jsx | 18 -- .../tests/BudgetCard.test.jsx | 2 +- .../tests/LearnerCreditManagement.test.jsx | 214 ------------------ .../tests/OfferNameHeading.test.jsx | 20 -- 11 files changed, 51 insertions(+), 616 deletions(-) delete mode 100644 src/components/learner-credit-management/BudgetCard-V2.jsx delete mode 100644 src/components/learner-credit-management/LearnerCreditManagement.jsx delete mode 100644 src/components/learner-credit-management/OfferNameHeading.jsx delete mode 100644 src/components/learner-credit-management/tests/LearnerCreditManagement.test.jsx delete mode 100644 src/components/learner-credit-management/tests/OfferNameHeading.test.jsx diff --git a/package-lock.json b/package-lock.json index 0a62f3ad47..86dab080e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,6 @@ "react-redux": "7.1.1", "react-router": "5.2.0", "react-router-dom": "5.2.0", - "react-textarea-autosize": "7.1.2", "react-truncate": "^2.4.0", "redux": "4.0.4", "redux-devtools-extension": "2.13.8", @@ -19392,18 +19391,6 @@ "object-assign": "^4.1.1" } }, - "node_modules/react-textarea-autosize": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.2.tgz", - "integrity": "sha512-uH3ORCsCa3C6LHxExExhF4jHoXYCQwE5oECmrRsunlspaDAbS4mGKNlWZqjLfInWtFQcf0o1n1jC/NGXFdUBCg==", - "dependencies": { - "@babel/runtime": "^7.1.2", - "prop-types": "^15.6.0" - }, - "peerDependencies": { - "react": ">=0.14.0 <17.0.0" - } - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index d042436e6c..9cca08b084 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "react-redux": "7.1.1", "react-router": "5.2.0", "react-router-dom": "5.2.0", - "react-textarea-autosize": "7.1.2", "react-truncate": "^2.4.0", "redux": "4.0.4", "redux-devtools-extension": "2.13.8", diff --git a/src/components/TextAreaAutoSize/index.jsx b/src/components/TextAreaAutoSize/index.jsx index 0963f73c7a..1d08b1cd54 100644 --- a/src/components/TextAreaAutoSize/index.jsx +++ b/src/components/TextAreaAutoSize/index.jsx @@ -25,6 +25,7 @@ const TextAreaAutoSize = ({ isValid={touched && !error} isInvalid={hasError} rows={3} + autoResize data-hj-suppress /> {hasError && {error}} diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx deleted file mode 100644 index 41ae121150..0000000000 --- a/src/components/learner-credit-management/BudgetCard-V2.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useOfferSummary } from './data'; -import SubBudgetCard from './SubBudgetCard'; -import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; - -/** - * Renders one or more budget cards for the given offer (enterprise or Subsidy from enterprise-subsidy). If the offer is - * an enterprise offer, it will render a single card. If the offer is a Subsidy, it will render one card for - * each associated budget. - * - * @param {*} offer Represents either an enterprise offer or a Subsidy (enterprise-subsidy). - * @returns Budget card component(s). - */ -const BudgetCard = ({ - offer, - enterpriseUUID, - enterpriseSlug, - offerType, - displayName, -}) => { - const { start, end } = offer; - - const { - isLoading: isLoadingOfferSummary, - offerSummary, - } = useOfferSummary(enterpriseUUID, offer); - - // Enterprise Offers will always have a single budget, so we can render a single card. - if (offerType === BUDGET_TYPES.ecommerce) { - return ( - - ); - } - - // We're now dealing with a Subsidy (enterprise-subsidy), but the analytics API isn't aware of any - // associated budgets; nothing should display. - if (!offerSummary?.budgetsSummary) { - return null; - } - - // Render a card for each associated budget with the Subsidy (enterprise-subsidy) - return offerSummary.budgetsSummary.map((budget) => ( - - )); -}; - -BudgetCard.propTypes = { - offer: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - start: PropTypes.string.isRequired, - end: PropTypes.string.isRequired, - }).isRequired, - enterpriseUUID: PropTypes.string.isRequired, - enterpriseSlug: PropTypes.string.isRequired, - offerType: PropTypes.oneOf(Object.values(BUDGET_TYPES)).isRequired, - displayName: PropTypes.string, -}; - -export default BudgetCard; diff --git a/src/components/learner-credit-management/BudgetCard.jsx b/src/components/learner-credit-management/BudgetCard.jsx index caef8e13c4..41ae121150 100644 --- a/src/components/learner-credit-management/BudgetCard.jsx +++ b/src/components/learner-credit-management/BudgetCard.jsx @@ -1,168 +1,67 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import dayjs from 'dayjs'; -import { - Card, - Button, - Stack, - Row, - Col, - Breadcrumb, -} from '@edx/paragon'; - -import { getCourseProductLineAbbreviation } from '../../utils'; -import { useOfferRedemptions, useOfferSummary } from './data'; -import LearnerCreditAggregateCards from './LearnerCreditAggregateCards'; -import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; -import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; -import { EXEC_ED_OFFER_TYPE } from './data/constants'; +import { useOfferSummary } from './data'; +import SubBudgetCard from './SubBudgetCard'; +import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; +/** + * Renders one or more budget cards for the given offer (enterprise or Subsidy from enterprise-subsidy). If the offer is + * an enterprise offer, it will render a single card. If the offer is a Subsidy, it will render one card for + * each associated budget. + * + * @param {*} offer Represents either an enterprise offer or a Subsidy (enterprise-subsidy). + * @returns Budget card component(s). + */ const BudgetCard = ({ offer, enterpriseUUID, enterpriseSlug, + offerType, + displayName, }) => { - const { - start, - end, - } = offer; + const { start, end } = offer; const { isLoading: isLoadingOfferSummary, offerSummary, } = useOfferSummary(enterpriseUUID, offer); - const { - isLoading: isLoadingOfferRedemptions, - offerRedemptions, - fetchOfferRedemptions, - } = useOfferRedemptions(enterpriseUUID, offer?.id); - const [detailPage, setDetailPage] = useState(false); - const [activeLabel, setActiveLabel] = useState(''); - const links = [ - { label: 'Budgets', url: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}` }, - ]; - const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); - const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); - const navigateToBudgetRedemptions = (budgetType) => { - setDetailPage(true); - links.push({ label: budgetType, url: `/${enterpriseSlug}/admin/learner-credit` }); - setActiveLabel(budgetType); - }; - - const renderActions = (budgetType) => ( - - ); - - const renderCardHeader = (budgetType) => { - const subtitle = ( -
- - {formattedStartDate} - {formattedExpirationDate} - -
- ); - + // Enterprise Offers will always have a single budget, so we can render a single card. + if (offerType === BUDGET_TYPES.ecommerce) { return ( - - {renderActions(budgetType)} -
- )} + ); - }; - - const renderCardSection = (available, spent) => ( - - - - Available - {available} - - - Spent - {spent} - - - - ); + } - const renderCardAggregate = () => ( -
- -
- ); + // We're now dealing with a Subsidy (enterprise-subsidy), but the analytics API isn't aware of any + // associated budgets; nothing should display. + if (!offerSummary?.budgetsSummary) { + return null; + } - return ( - - - - - - - {!detailPage - ? ( - <> - {renderCardAggregate()} -

Budgets

- - - - {renderCardHeader('Open Courses Marketplace')} - {renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFundsOcm)} - - - - {offerSummary?.offerType === EXEC_ED_OFFER_TYPE - && ( - - - - {renderCardHeader('Executive Education')} - {renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFundsExecEd)} - - - - )} - - ) - : ( - - )} -
- ); + // Render a card for each associated budget with the Subsidy (enterprise-subsidy) + return offerSummary.budgetsSummary.map((budget) => ( + + )); }; BudgetCard.propTypes = { @@ -174,6 +73,8 @@ BudgetCard.propTypes = { }).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, + offerType: PropTypes.oneOf(Object.values(BUDGET_TYPES)).isRequired, + displayName: PropTypes.string, }; export default BudgetCard; diff --git a/src/components/learner-credit-management/LearnerCreditManagement.jsx b/src/components/learner-credit-management/LearnerCreditManagement.jsx deleted file mode 100644 index 4786f3e72f..0000000000 --- a/src/components/learner-credit-management/LearnerCreditManagement.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { - useContext, useEffect, -} from 'react'; -import PropTypes from 'prop-types'; -import Helmet from 'react-helmet'; -import { connect } from 'react-redux'; -import dayjs from 'dayjs'; -import { - Badge, - Container, - Stack, - Skeleton, -} from '@edx/paragon'; -import { logError } from '@edx/frontend-platform/logging'; - -import Hero from '../Hero'; -import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; -import { NotFound } from '../NotFoundPage'; -import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; -import LearnerCreditAggregateCards from './LearnerCreditAggregateCards'; -import LearnerCreditDisclaimer from './LearnerCreditDisclaimer'; -import OfferDates from './OfferDates'; -import OfferNameHeading from './OfferNameHeading'; -import { useOfferSummary, useOfferRedemptions } from './data'; -import { DATE_FORMAT } from './data/constants'; -import OfferUtilizationAlerts from './OfferUtilizationAlerts'; - -const LearnerCreditManagement = ({ enterpriseUUID }) => { - const { offers } = useContext(EnterpriseSubsidiesContext); - const enterpriseOffer = offers[0]; - - const { - isLoading: isLoadingOfferSummary, - offerSummary, - } = useOfferSummary(enterpriseUUID, enterpriseOffer); - const { - isLoading: isLoadingOfferRedemptions, - offerRedemptions, - fetchOfferRedemptions, - } = useOfferRedemptions(enterpriseUUID, enterpriseOffer?.id); - - /** - * Log error only once when no offer exists. - */ - useEffect(() => { - if (offers.length === 0) { - logError(`"Learner Credit Management" accessed with no enterprise offer configured for enterprise ${enterpriseUUID}.`); - } - }, [offers, enterpriseUUID]); - - if (!enterpriseOffer) { - return ; - } - - // The LPR data is synced once per day, and all its data is fresh, meaning we can - // deduce when the data was last updated based on when any of the offer redemptions - // records were created. - const offerDataLastUpdatedTimestamp = offerRedemptions.results[0]?.created; - return ( - <> - - Learner Credit Management - - - - - -
- - {enterpriseOffer.isCurrent ? ( - Active - ) : ( - Ended - )} - - -
- {isLoadingOfferSummary || isLoadingOfferRedemptions ? ( - - ) : ( - - )} -
- -
-
- -
-
- - ); -}; - -const mapStateToProps = state => ({ - enterpriseUUID: state.portalConfiguration.enterpriseId, -}); - -LearnerCreditManagement.propTypes = { - enterpriseUUID: PropTypes.string.isRequired, -}; - -export default connect(mapStateToProps)(LearnerCreditManagement); diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index 44407531c0..b8220c4768 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -6,7 +6,7 @@ import { Col, } from '@edx/paragon'; -import BudgetCard from './BudgetCard-V2'; +import BudgetCard from './BudgetCard'; import { orderOffers } from './data/utils'; const MultipleBudgetsPicker = ({ diff --git a/src/components/learner-credit-management/OfferNameHeading.jsx b/src/components/learner-credit-management/OfferNameHeading.jsx deleted file mode 100644 index 09af054c4b..0000000000 --- a/src/components/learner-credit-management/OfferNameHeading.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const OfferNameHeading = ({ name }) => ( -

- {name} -

-); - -OfferNameHeading.propTypes = { - name: PropTypes.string, -}; - -OfferNameHeading.defaultProps = { - name: 'Overview', -}; - -export default OfferNameHeading; diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index a2bc074944..7ddc100c8e 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -12,7 +12,7 @@ import { import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import BudgetCard from '../BudgetCard-V2'; +import BudgetCard from '../BudgetCard'; import { useOfferSummary, useOfferRedemptions } from '../data'; import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; diff --git a/src/components/learner-credit-management/tests/LearnerCreditManagement.test.jsx b/src/components/learner-credit-management/tests/LearnerCreditManagement.test.jsx deleted file mode 100644 index f268b2faf7..0000000000 --- a/src/components/learner-credit-management/tests/LearnerCreditManagement.test.jsx +++ /dev/null @@ -1,214 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react'; -import { Provider } from 'react-redux'; -import thunk from 'redux-thunk'; -import configureMockStore from 'redux-mock-store'; -import dayjs from 'dayjs'; -import { - screen, - render, -} from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import LearnerCreditManagement from '../LearnerCreditManagement'; -import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; -import { DATE_FORMAT } from '../data/constants'; -import { useOfferSummary, useOfferRedemptions } from '../data/hooks'; - -jest.mock('../data/hooks'); -useOfferSummary.mockReturnValue({ - isLoading: false, - offerSummary: null, -}); -useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: { - itemCount: 0, - pageCount: 0, - results: [], - }, - fetchOfferRedemptions: jest.fn(), -}); - -jest.mock('../../NotFoundPage', () => ({ - __esModule: true, - NotFound: () => , -})); - -jest.mock('../OfferNameHeading', () => ({ - __esModule: true, - default: ({ name }) => {name}, -})); - -jest.mock('../OfferDates', () => ({ - __esModule: true, - default: ({ start, end }) => ( - <> - {start} - {end} - - ), -})); - -jest.mock('../LearnerCreditAllocationTable', () => ({ - __esModule: true, - default: ({ - enterpriseUUID, isLoading, tableData, fetchTableData, - }) => ( - <> - {isLoading ? 'is loading' : 'is NOT loading'} - {tableData?.results[0]?.enterpriseEnrollmentId} - {typeof fetchTableData} - {enterpriseUUID} - - ), -})); - -jest.mock('../LearnerCreditAggregateCards', () => ({ - __esModule: true, - default: ({ - isLoading, totalFunds, redeemedFunds, remainingFunds, percentUtilized, - }) => ( - <> - {isLoading ? 'is loading' : 'is NOT loading'} - {totalFunds} - {redeemedFunds} - {remainingFunds} - {percentUtilized} - - ), -})); - -const mockStore = configureMockStore([thunk]); -const getMockStore = store => mockStore(store); -const enterpriseId = 'test-enterprise'; -const initialStore = { - portalConfiguration: { - enterpriseId, - }, -}; -const store = getMockStore({ ...initialStore }); - -const mockEnterpriseOfferId = 123; -const mockEnterpriseOfferEnrollmentId = 456; -const defaultEnterpriseSubsidiesContextValue = { - offers: [], -}; - -const mockOfferDisplayName = 'Test Enterprise Offer'; -const mockOfferSummary = { - totalFunds: 5000, - redeemedFunds: 200, - remainingFunds: 4800, - percentUtilized: 0.04, -}; - -const LearnerCreditManagementWrapper = ({ - enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue, - ...rest -}) => ( - - - - - -); - -describe('', () => { - it('displays not found page with no enterprise offer', () => { - render(); - expect(screen.getByTestId('404-page-not-found')); - }); - - describe('with enterprise offer', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('displays correctly', () => { - const mockOffer = { - id: mockEnterpriseOfferId, - name: mockOfferDisplayName, - start: '2022-01-01', - end: '2023-01-01', - }; - const mockOfferRedemption = { - created: '2022-02-01', - enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, - userEmail: 'test@example.com', - courseTitle: 'edX Demonstration Course', - courseListPrice: 100, - enrollmentDate: '2022-01-01', - uuid: '123abc-abc123', - }; - const subsidiesContextValue = { - offers: [mockOffer], - }; - useOfferSummary.mockReturnValue({ - isLoading: false, - offerSummary: mockOfferSummary, - }); - useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: { - results: [mockOfferRedemption], - itemCount: 1, - pageCount: 1, - }, - fetchOfferRedemptions: jest.fn(), - }); - render(); - expect(screen.queryByTestId('404-page-not-found')).toBeFalsy(); - expect(screen.getByText('Learner Credit Management')); - expect(screen.getByText(mockOffer.name)); - - expect(screen.getByText(mockOffer.start)); - expect(screen.getByText(mockOffer.end)); - - expect(screen.getByText(`Data last updated on ${dayjs(mockOfferRedemption.created).format(DATE_FORMAT)}`, { exact: false })); - - expect(screen.getByTestId('learner-credit-allocation--is-loading')).toHaveTextContent('is NOT loading'); - expect(screen.getByTestId('learner-credit-allocation--table-data')).toHaveTextContent(mockOfferRedemption.enterpriseEnrollmentId); - expect(screen.getByTestId('learner-credit-allocation--fetch-table-data')).toHaveTextContent('function'); - expect(screen.getByTestId('learner-credit-allocation--enterprise-uuid')).toHaveTextContent(enterpriseId); - - expect(screen.getByTestId('learner-credit-aggregate-cards--loading')).toHaveTextContent('is NOT loading'); - expect(screen.getByTestId('learner-credit-aggregate-cards--total-funds')).toHaveTextContent('5000'); - expect(screen.getByTestId('learner-credit-aggregate-cards--redeemed-funds')).toHaveTextContent('200'); - expect(screen.getByTestId('learner-credit-aggregate-cards--remaining-funds')).toHaveTextContent('4800'); - expect(screen.getByTestId('learner-credit-aggregate-cards--percent-utilized')).toHaveTextContent('0.04'); - }); - - describe('status badge', () => { - it('with non-current offer', () => { - const subsidiesContextValue = { - offers: [{ - id: mockEnterpriseOfferId, - isCurrent: false, - }], - }; - useOfferSummary.mockReturnValue = { - isLoading: false, - offerSummary: mockOfferSummary, - }; - render(); - expect(screen.getByText('Ended')); - }); - - it('with current offer', () => { - const subsidiesContextValue = { - offers: [{ - id: mockEnterpriseOfferId, - isCurrent: true, - }], - }; - useOfferSummary.mockReturnValue = { - isLoading: false, - offerSummary: mockOfferSummary, - }; - render(); - expect(screen.getByText('Active')); - }); - }); - }); -}); diff --git a/src/components/learner-credit-management/tests/OfferNameHeading.test.jsx b/src/components/learner-credit-management/tests/OfferNameHeading.test.jsx deleted file mode 100644 index 913d92bb8d..0000000000 --- a/src/components/learner-credit-management/tests/OfferNameHeading.test.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { - screen, - render, -} from '@testing-library/react'; - -import OfferNameHeading from '../OfferNameHeading'; - -describe('', () => { - it('with offer name present, display it', () => { - const offerName = 'Test Enterprise Offer Title'; - render(); - expect(screen.getByText(offerName)); - }); - - it('without offer name present, fallback to "Overview"', async () => { - render(); - expect(screen.getByText('Overview')); - }); -}); From 114a3601ec7800e54d4ad9337f1807250759655f Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Mon, 30 Oct 2023 12:46:51 -0400 Subject: [PATCH 050/124] feat: add status and recent action columns to assigned table (#1071) --- .../AssignmentRecentActionTableCell.jsx | 26 +++ .../AssignmentStatusTableCell.jsx | 63 +++++++ .../BudgetAssignmentsTable.jsx | 12 ++ .../BudgetDetailAssignments.jsx | 2 +- .../BaseModalPopup.jsx | 82 +++++++++ .../FailedBadEmail.jsx | 63 +++++++ .../assignments-status-chips/FailedSystem.jsx | 52 ++++++ .../NotifyingLearner.jsx | 45 +++++ .../WaitingForLearner.jsx | 55 ++++++ .../data/constants.js | 3 + .../AssignMoreCoursesEmptyStateMinimal.scss | 14 -- .../styles/index.scss | 22 +++ .../tests/BudgetDetailPage.test.jsx | 156 ++++++++++++++++++ src/index.scss | 2 +- 14 files changed, 581 insertions(+), 16 deletions(-) create mode 100644 src/components/learner-credit-management/AssignmentRecentActionTableCell.jsx create mode 100644 src/components/learner-credit-management/AssignmentStatusTableCell.jsx create mode 100644 src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx create mode 100644 src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx create mode 100644 src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx create mode 100644 src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx create mode 100644 src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx delete mode 100644 src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss create mode 100644 src/components/learner-credit-management/styles/index.scss diff --git a/src/components/learner-credit-management/AssignmentRecentActionTableCell.jsx b/src/components/learner-credit-management/AssignmentRecentActionTableCell.jsx new file mode 100644 index 0000000000..2fef6f39f2 --- /dev/null +++ b/src/components/learner-credit-management/AssignmentRecentActionTableCell.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { formatDate } from './data'; + +const AssignmentRecentActionTableCell = ({ row }) => { + const { original: { recentAction } } = row; + const { actionType, timestamp } = recentAction; + const formattedActionType = `${actionType.charAt(0).toUpperCase()}${actionType.slice(1)}`; + const formattedActionTimestamp = formatDate(timestamp); + return ( + {formattedActionType}: {formattedActionTimestamp} + ); +}; + +AssignmentRecentActionTableCell.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + recentAction: PropTypes.shape({ + actionType: PropTypes.string.isRequired, + timestamp: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; + +export default AssignmentRecentActionTableCell; diff --git a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx new file mode 100644 index 0000000000..5ee9f6158e --- /dev/null +++ b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Chip, +} from '@edx/paragon'; +import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; +import WaitingForLearner from './assignments-status-chips/WaitingForLearner'; +import FailedBadEmail from './assignments-status-chips/FailedBadEmail'; +import FailedSystem from './assignments-status-chips/FailedSystem'; + +const AssignmentStatusTableCell = ({ row }) => { + const { original: { learnerEmail, learnerState } } = row; + + // Learner state is not available for this assignment, so don't display anything. + if (!learnerState) { + return null; + } + + if (learnerState === 'notifying') { + return ( + + ); + } + + if (learnerState === 'waiting') { + return ( + + ); + } + + if (learnerState === 'failed') { + // Determine the failure reason based on the actions. + const { actions } = row.original; + const mostRecentAction = actions[0]; // API returns actions in reverse chronological order. + const isBadEmailError = mostRecentAction.actionType === 'notified' && !!mostRecentAction.errorReason; + + if (isBadEmailError) { + return ( + + ); + } + + return ; + } + + // Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway. + return {`${learnerState.charAt(0).toUpperCase()}${learnerState.substr(1)}`}; +}; + +AssignmentStatusTableCell.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + learnerEmail: PropTypes.string, + learnerState: PropTypes.string.isRequired, + actions: PropTypes.arrayOf(PropTypes.shape({ + actionType: PropTypes.string.isRequired, + errorReason: PropTypes.string, + })).isRequired, + }).isRequired, + }).isRequired, +}; + +export default AssignmentStatusTableCell; diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index cbb7596feb..1b8917ea0e 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -5,7 +5,9 @@ import { DataTable } from '@edx/paragon'; import TableTextFilter from './TableTextFilter'; import CustomDataTableEmptyState from './CustomDataTableEmptyState'; import AssignmentDetailsTableCell from './AssignmentDetailsTableCell'; +import AssignmentStatusTableCell from './AssignmentStatusTableCell'; import { DEFAULT_PAGE, PAGE_SIZE, formatPrice } from './data'; +import AssignmentRecentActionTableCell from './AssignmentRecentActionTableCell'; const FilterStatus = (rest) => ; @@ -36,6 +38,16 @@ const BudgetAssignmentsTable = ({ Cell: ({ row }) => `-${formatPrice(row.original.contentQuantity / 100, { maximumFractionDigits: 0 })}`, disableFilters: true, }, + { + Header: 'Status', + Cell: AssignmentStatusTableCell, + disableFilters: true, + }, + { + Header: 'Recent action', + Cell: AssignmentRecentActionTableCell, + disableFilters: true, + }, ]} initialTableOptions={{ getRowId: row => row?.uuid?.toString(), diff --git a/src/components/learner-credit-management/BudgetDetailAssignments.jsx b/src/components/learner-credit-management/BudgetDetailAssignments.jsx index 218ac84d5d..7e67eb5672 100644 --- a/src/components/learner-credit-management/BudgetDetailAssignments.jsx +++ b/src/components/learner-credit-management/BudgetDetailAssignments.jsx @@ -36,7 +36,7 @@ const BudgetDetailAssignments = ({ } return ( -
+

Assigned

Assigned activity earmarks funds in your budget so you can't overspend. For funds to move diff --git a/src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx b/src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx new file mode 100644 index 0000000000..ba25e18b6e --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Icon, ModalPopup } from '@edx/paragon'; +import { ASSIGNMENT_STATUS_MODAL_MAX_WIDTH } from '../data'; + +export const BaseModalPopupHeading = ({ icon, iconClassName, children }) => ( +

+

+ + {children} +

+
+); + +export const BaseModalPopupContent = ({ children }) => ( + <> +
+
+ {children} +
+ +); + +const BaseModalPopup = ({ + placement, + positionRef, + isOpen, + onClose, + children, + ...rest +}) => ( + +
+
+ {children} +
+
+
+); + +BaseModalPopup.Heading = BaseModalPopupHeading; +BaseModalPopup.Content = BaseModalPopupContent; + +BaseModalPopupHeading.propTypes = { + icon: PropTypes.elementType.isRequired, + iconClassName: PropTypes.string, + children: PropTypes.node.isRequired, +}; + +BaseModalPopupHeading.defaultProps = { + iconClassName: undefined, +}; + +BaseModalPopupContent.propTypes = { + children: PropTypes.node.isRequired, +}; + +BaseModalPopup.propTypes = { + placement: PropTypes.string, + positionRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + children: PropTypes.node.isRequired, +}; + +BaseModalPopup.defaultProps = { + placement: 'auto', + positionRef: null, +}; + +export default BaseModalPopup; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx new file mode 100644 index 0000000000..22228de922 --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; + +import BaseModalPopup from './BaseModalPopup'; + +const FailedBadEmail = ({ learnerEmail }) => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = useState(null); + + return ( + <> + + Failed: Bad email + + + + Failed: Bad email + + +

+ This course assignment failed because a notification to {learnerEmail || 'the learner'} could not be sent. +

+
+

Resolution steps

+
    +
  • + Cancel this assignment to release the associated Learner Credit funds into your available balance. +
  • +
  • + Get more troubleshooting help at{' '} + + Help Center: Course Assignments + . +
  • +
+
+
+
+ + ); +}; + +FailedBadEmail.propTypes = { + learnerEmail: PropTypes.string, +}; + +FailedBadEmail.defaultProps = { + learnerEmail: undefined, +}; + +export default FailedBadEmail; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx new file mode 100644 index 0000000000..cfee2cb7bb --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; + +import BaseModalPopup from './BaseModalPopup'; + +const FailedSystem = () => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = useState(null); + + return ( + <> + + Failed: System + + + + Failed: System + + +

Something went wrong behind the scenes.

+
+

Resolution steps

+
    +
  • + Cancel this assignment to release the associated Learner Credit funds into your available balance. +
  • +
  • + Get more troubleshooting help at{' '} + + Help Center: Course Assignments + . +
  • +
+
+
+
+ + ); +}; + +export default FailedSystem; diff --git a/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx new file mode 100644 index 0000000000..2380fda362 --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Chip, useToggle } from '@edx/paragon'; +import { Send } from '@edx/paragon/icons'; +import BaseModalPopup from './BaseModalPopup'; + +const NotifyingLearner = ({ learnerEmail }) => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = useState(null); + + return ( + <> + + Notifying learner + + + + Notifying {learnerEmail ?? 'learner'} + + +

+ Our system is busy emailing {learnerEmail ?? 'the learner'}! Refresh in a few minutes to + confirm the assignment notification was successful. +

+
+
+ + ); +}; + +NotifyingLearner.propTypes = { + learnerEmail: PropTypes.string, +}; + +export default NotifyingLearner; diff --git a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx new file mode 100644 index 0000000000..1930262097 --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Timelapse } from '@edx/paragon/icons'; + +import BaseModalPopup from './BaseModalPopup'; + +const WaitingForLearner = ({ learnerEmail }) => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = useState(null); + + return ( + <> + + Waiting for learner + + + + Waiting for {learnerEmail ?? 'learner'} + + +

+ This learner must create an edX account and complete enrollment in the course before the + enrollment deadline or within 90 days of assignment, whichever is sooner. +

+
+

Need help?

+

+ Learn more about learner enrollment in assigned courses at{' '} + + Help Center: Course Assignments + . +

+
+
+
+ + ); +}; + +WaitingForLearner.propTypes = { + learnerEmail: PropTypes.string, +}; + +export default WaitingForLearner; diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 08ad16ef5d..f448267938 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -44,6 +44,9 @@ export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array // Number of items to display per page in Budget Catalog tab export const SEARCH_RESULT_PAGE_SIZE = 15; +// Max width of Assigned table status column's modalpopup dialog; matches `Popover`. +export const ASSIGNMENT_STATUS_MODAL_MAX_WIDTH = 480; + // Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`. // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const learnerCreditManagementQueryKeys = { diff --git a/src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss b/src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss deleted file mode 100644 index 8a1e4db6e7..0000000000 --- a/src/components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal.scss +++ /dev/null @@ -1,14 +0,0 @@ -// The `Card` component in Paragon does not seem to properly let consumers customize the width of the `Card.Body` -// contents when in the horizontal card orientation without custom CSS. As a result, both the `Card.Footer` and -// `Card.Body` incorrectly get equal column widths when the preference is that the `Card.Body` has more width than -// the `Card.Footer`. The below styles force the `Card.Body` to have appropriately more width than the `Card.Footer` when -// the `Card` is in the horizontal orientation. - -.assign-more-courses-empty-state-minimal { - .assign-more-courses__card-body { - flex: 3; - } - .assign-more-courses__card-footer { - flex: 1; - } -} diff --git a/src/components/learner-credit-management/styles/index.scss b/src/components/learner-credit-management/styles/index.scss new file mode 100644 index 0000000000..c3301e7af2 --- /dev/null +++ b/src/components/learner-credit-management/styles/index.scss @@ -0,0 +1,22 @@ +.budget-detail-assignments { + // This is a (temporary) workaround to ensure the `Chip` modal popups within the "Assigned" table status column + // properly overlays the underlying `DataTable`. + .pgn__data-table-container, + .pgn__data-table-layout-wrapper { + overflow-x: visible; + } + + // The `Card` component in Paragon does not seem to properly let consumers customize the width of the `Card.Body` + // contents when in the horizontal card orientation without custom CSS. As a result, both the `Card.Footer` and + // `Card.Body` incorrectly get equal column widths when the preference is that the `Card.Body` has more width than + // the `Card.Footer`. The below styles force the `Card.Body` to have appropriately more width than the `Card.Footer` when + // the `Card` is in the horizontal orientation. + .assign-more-courses-empty-state-minimal { + .assign-more-courses__card-body { + flex: 3; + } + .assign-more-courses__card-footer { + flex: 1; + } + } +} diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index fd87153c21..4264b686f4 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -18,6 +18,7 @@ import { useBudgetContentAssignments, useBudgetDetailActivityOverview, useIsLargeOrGreater, + formatDate, } from '../data'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; import { @@ -59,6 +60,7 @@ const initialStoreState = { const mockLearnerEmail = 'edx@example.com'; const mockCourseKey = 'edX+DemoX'; const mockContentTitle = 'edx Demo'; + const mockEmptyStateBudgetDetailActivityOverview = { contentAssignments: { count: 0 }, spentTransactions: { count: 0 }, @@ -68,6 +70,24 @@ const mockEmptyOfferRedemptions = { pageCount: 0, results: [], }; +const mockSuccessfulNotifiedAction = { + uuid: 'test-assignment-action-uuid', + actionType: 'notified', + completedAt: '2023-10-27', + errorReason: null, +}; + +const mockFailedNotifiedAction = { + ...mockSuccessfulNotifiedAction, + completedAt: null, + errorReason: 'bad_email', +}; + +const mockFailedLinkedLearnerAction = { + ...mockFailedNotifiedAction, + actionType: 'learner_linked', + errorReason: 'internal_api_error', +}; const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; @@ -275,6 +295,10 @@ describe('', () => { learnerEmail: mockLearnerEmail, contentKey: mockCourseKey, contentTitle: mockContentTitle, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulNotifiedAction], }, ], numPages: 1, @@ -296,6 +320,9 @@ describe('', () => { const viewCourseCTA = assignedSection.getByText(mockContentTitle, { selector: 'a' }); expect(viewCourseCTA).toBeInTheDocument(); expect(viewCourseCTA.getAttribute('href')).toEqual(`${process.env.ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${mockCourseKey}`); + expect(assignedSection.getByText('-$199')).toBeInTheDocument(); + expect(assignedSection.getByText('Waiting for learner')).toBeInTheDocument(); + expect(assignedSection.getByText(`Assigned: ${formatDate('2023-10-27')}`)).toBeInTheDocument(); }); it('renders with assigned table data "View Course" hyperlink default when content title is null', () => { @@ -323,6 +350,11 @@ describe('', () => { uuid: 'test-uuid', learnerEmail: mockLearnerEmail, contentKey: mockCourseKey, + contentTitle: null, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulNotifiedAction], }, ], numPages: 1, @@ -346,6 +378,130 @@ describe('', () => { expect(viewCourseCTA.getAttribute('href')).toEqual(`${process.env.ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${mockCourseKey}`); }); + it.each([ + { + learnerState: 'notifying', + hasLearnerEmail: true, + expectedChipStatus: 'Notifying learner', + expectedModalPopupHeading: `Notifying ${mockLearnerEmail}`, + expectedModalPopupContent: `Our system is busy emailing ${mockLearnerEmail}!`, + actions: [], + }, + { + learnerState: 'notifying', + hasLearnerEmail: false, + expectedChipStatus: 'Notifying learner', + expectedModalPopupHeading: 'Notifying learner', + expectedModalPopupContent: 'Our system is busy emailing the learner!', + actions: [], + }, + { + learnerState: 'waiting', + hasLearnerEmail: true, + expectedChipStatus: 'Waiting for learner', + expectedModalPopupHeading: `Waiting for ${mockLearnerEmail}`, + expectedModalPopupContent: 'This learner must create an edX account and complete enrollment in the course', + actions: [mockSuccessfulNotifiedAction], + }, + { + learnerState: 'waiting', + hasLearnerEmail: false, + expectedChipStatus: 'Waiting for learner', + expectedModalPopupHeading: 'Waiting for learner', + expectedModalPopupContent: 'This learner must create an edX account and complete enrollment in the course', + actions: [mockSuccessfulNotifiedAction], + }, + { + learnerState: 'failed', + hasLearnerEmail: true, + expectedChipStatus: 'Failed: Bad email', + expectedModalPopupHeading: 'Failed: Bad email', + expectedModalPopupContent: `This course assignment failed because a notification to ${mockLearnerEmail} could not be sent.`, + actions: [mockFailedNotifiedAction], + }, + { + learnerState: 'failed', + hasLearnerEmail: false, + expectedChipStatus: 'Failed: Bad email', + expectedModalPopupHeading: 'Failed: Bad email', + expectedModalPopupContent: 'This course assignment failed because a notification to the learner could not be sent.', + actions: [mockFailedNotifiedAction], + }, + { + learnerState: 'failed', + hasLearnerEmail: true, + expectedChipStatus: 'Failed: System', + expectedModalPopupHeading: 'Failed: System', + expectedModalPopupContent: 'Something went wrong behind the scenes.', + actions: [mockFailedLinkedLearnerAction], + }, + ])('renders correct status chips with assigned table data (%s)', ({ + learnerState, + hasLearnerEmail, + expectedChipStatus, + expectedModalPopupHeading, + expectedModalPopupContent, + actions, + }) => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [ + { + uuid: 'test-uuid', + learnerEmail: hasLearnerEmail ? mockLearnerEmail : null, + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState, + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions, + }, + ], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); + renderWithRouter(); + + // Assigned table is visible within Activity tab contents + const assignedSection = within(screen.getByText('Assigned').closest('section')); + if (hasLearnerEmail) { + expect(assignedSection.getByText(mockLearnerEmail)).toBeInTheDocument(); + } else { + expect(assignedSection.getByText('Email hidden')).toBeInTheDocument(); + } + const statusChip = assignedSection.getByText(expectedChipStatus); + expect(statusChip).toBeInTheDocument(); + userEvent.click(statusChip); + + // Modal popup is visible with expected text + const modalPopupContents = within(screen.getByTestId('assignment-status-modalpopup-contents')); + expect(modalPopupContents.getByText(expectedModalPopupHeading)).toBeInTheDocument(); + expect(modalPopupContents.getByText(expectedModalPopupContent, { exact: false })).toBeInTheDocument(); + }); + it('renders with catalog tab active on initial load for assignable budgets', async () => { useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, diff --git a/src/index.scss b/src/index.scss index dfdae9bc8e..c2bfdfb974 100644 --- a/src/index.scss +++ b/src/index.scss @@ -24,7 +24,7 @@ $modal-max-width: 650px; @import "./components/BulkEnrollmentPage/BulkEnrollment"; @import "./components/Admin/Admin"; @import "./components/settings/settings"; -@import "./components/learner-credit-management/styles/AssignMoreCoursesEmptyStateMinimal"; +@import "./components/learner-credit-management/styles"; body { overflow-x: hidden; From a33a21a475cab404ab77883d6e24fe25710c3903 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Mon, 30 Oct 2023 15:03:11 -0400 Subject: [PATCH 051/124] fix: use top placement, not auto (#1076) --- .../assignments-status-chips/BaseModalPopup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx b/src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx index ba25e18b6e..23f528d443 100644 --- a/src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/BaseModalPopup.jsx @@ -75,7 +75,7 @@ BaseModalPopup.propTypes = { }; BaseModalPopup.defaultProps = { - placement: 'auto', + placement: 'top', positionRef: null, }; From 5d2a73667ca2eed835404bab0f77faf3e4d26d55 Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:34:24 -0700 Subject: [PATCH 052/124] feat: add course/program detail to 'View Course' button (#1065) * feat: display search result cards in catalog tab * fix: failing test in BudgetDetailPage * fix: replace word register with enroll * fix: implemented reviewer comments * fix: lint error * fix: lint error * feat: added policy's catalog uuid to search filter * feat: implement view course button to learn more * fix: failing test * fix: added test * fix: added test coverage * fix: refactored based on reviewer feedback * fix: lint error * fix: refactored code to include new api field and updated test * fix: removing unused prop in test * fix: refactored * fix: search filters * chore: rebase * chore: refactored * chore: fix lint error * chore: refactored * fix: updated failing test --- .../BudgetDetailCatalogTabContents.jsx | 9 +-- .../cards/CourseCard.jsx | 26 +++++-- .../cards/CourseCard.test.jsx | 67 ++++++++++++++----- .../search/CatalogSearch.jsx | 15 ++--- .../tests/CatalogSearch.test.jsx | 36 +++++++--- .../tests/CatalogSearchResults.test.jsx | 42 ++++++++---- 6 files changed, 134 insertions(+), 61 deletions(-) diff --git a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx index a143c1f07a..ee67ecb6de 100644 --- a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx @@ -8,17 +8,10 @@ import CatalogSearch from './search/CatalogSearch'; import { LANGUAGE_REFINEMENT, LEARNING_TYPE_REFINEMENT, - useBudgetId, - useSubsidyAccessPolicy, } from './data'; import { configuration } from '../../config'; const BudgetDetailCatalogTabContents = () => { - const { subsidyAccessPolicyId } = useBudgetId(); - const { - data: subsidyAccessPolicy, - } = useSubsidyAccessPolicy(subsidyAccessPolicyId); - const language = { attribute: LANGUAGE_REFINEMENT, title: 'Language', @@ -48,7 +41,7 @@ const BudgetDetailCatalogTabContents = () => { indexName={configuration.ALGOLIA.INDEX_NAME} searchClient={searchClient} > - + diff --git a/src/components/learner-credit-management/cards/CourseCard.jsx b/src/components/learner-credit-management/cards/CourseCard.jsx index f4885ce433..2d98bf20eb 100644 --- a/src/components/learner-credit-management/cards/CourseCard.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.jsx @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ // variables taken from algolia not in camelcase -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; +import { AppContext } from '@edx/frontend-platform/react'; +import { connect } from 'react-redux'; import { Badge, @@ -21,17 +23,19 @@ import { formatPrice, formatDate, getEnrollmentDeadline } from '../data/utils'; import CARD_TEXT from '../constants'; const CourseCard = ({ - original, + original, enterpriseSlug, }) => { const { availability, cardImageUrl, courseType, + key, normalizedMetadata, partners, title, } = camelCaseObject(original); + const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext); const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); @@ -72,6 +76,13 @@ const CourseCard = ({ const isExecEd = courseType === EXEC_ED_COURSE_TYPE; + let linkToCourse; + if (isExecEd) { + linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`; + } else { + linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${key}`; + } + return ( + ); +}; + +AssignmentsTableRefreshAction.propTypes = { + refresh: PropTypes.func.isRequired, + tableInstance: PropTypes.shape({ + state: PropTypes.shape(), + }), +}; + +export default AssignmentsTableRefreshAction; diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index 1b8917ea0e..301dfc99f8 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -8,6 +8,7 @@ import AssignmentDetailsTableCell from './AssignmentDetailsTableCell'; import AssignmentStatusTableCell from './AssignmentStatusTableCell'; import { DEFAULT_PAGE, PAGE_SIZE, formatPrice } from './data'; import AssignmentRecentActionTableCell from './AssignmentRecentActionTableCell'; +import AssignmentsTableRefreshAction from './AssignmentsTableRefreshAction'; const FilterStatus = (rest) => ; @@ -35,7 +36,8 @@ const BudgetAssignmentsTable = ({ }, { Header: 'Amount', - Cell: ({ row }) => `-${formatPrice(row.original.contentQuantity / 100, { maximumFractionDigits: 0 })}`, + accessor: 'amount', + Cell: ({ row }) => `-${formatPrice(row.original.contentQuantity / 100)}`, disableFilters: true, }, { @@ -49,6 +51,9 @@ const BudgetAssignmentsTable = ({ disableFilters: true, }, ]} + tableActions={[ + , + ]} initialTableOptions={{ getRowId: row => row?.uuid?.toString(), }} diff --git a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx index e5f1309490..8b412b066a 100644 --- a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx +++ b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx @@ -1,13 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import dayjs from 'dayjs'; import { DataTable } from '@edx/paragon'; import TableTextFilter from './TableTextFilter'; import CustomDataTableEmptyState from './CustomDataTableEmptyState'; import SpendTableEnrollmentDetails from './SpendTableEnrollmentDetails'; -import { getCourseProductLineText } from '../../utils'; -import { PAGE_SIZE, DEFAULT_PAGE } from './data'; +import { + PAGE_SIZE, + DEFAULT_PAGE, + formatDate, + formatPrice, +} from './data'; const FilterStatus = (rest) => ; @@ -30,7 +33,7 @@ const LearnerCreditAllocationTable = ({ { Header: 'Date', accessor: 'enrollmentDate', - Cell: ({ row }) => dayjs(row.values.enrollmentDate).format('MMM D, YYYY'), + Cell: ({ row }) => formatDate(row.values.enrollmentDate), disableFilters: true, }, { @@ -42,13 +45,7 @@ const LearnerCreditAllocationTable = ({ { Header: 'Amount', accessor: 'courseListPrice', - Cell: ({ row }) => `$${row.values.courseListPrice}`, - disableFilters: true, - }, - { - Header: 'Product', - accessor: 'courseProductLine', - Cell: ({ row }) => getCourseProductLineText(row.values.courseProductLine), + Cell: ({ row }) => formatPrice(row.values.courseListPrice), disableFilters: true, }, ]} diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx index 44faafff34..fdd9bde51b 100644 --- a/src/components/learner-credit-management/search/CatalogSearchResults.jsx +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -9,7 +9,7 @@ import { } from '@edx/paragon'; import CourseCard from '../cards/CourseCard'; -import { SEARCH_RESULT_PAGE_SIZE } from '../data'; +import { DEFAULT_PAGE, SEARCH_RESULT_PAGE_SIZE } from '../data'; export const ERROR_MESSAGE = 'An error occurred while retrieving data'; @@ -87,7 +87,7 @@ export const BaseCatalogSearchResults = ({ defaultColumnValues={{ Filter: TextFilter }} initialState={{ pageSize: SEARCH_RESULT_PAGE_SIZE, - pageIndex: 0, + pageIndex: DEFAULT_PAGE, }} isLoading={isSearchStalled} isPaginated diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 253b0768c2..3c5574ec22 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -19,6 +19,8 @@ import { useBudgetDetailActivityOverview, useIsLargeOrGreater, formatDate, + DEFAULT_PAGE, + PAGE_SIZE, } from '../data'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; import { @@ -269,7 +271,7 @@ describe('', () => { expect(screen.getByText('Catalog').getAttribute('aria-selected')).toBe('false'); }); - it('renders with assigned table data', () => { + it('renders with assigned table data and handles table refresh', () => { useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', @@ -285,6 +287,7 @@ describe('', () => { spentTransactions: { count: 0 }, }, }); + const mockFetchContentAssignments = jest.fn(); useBudgetContentAssignments.mockReturnValue({ isLoading: false, contentAssignments: { @@ -304,7 +307,7 @@ describe('', () => { numPages: 1, currentPage: 1, }, - fetchContentAssignments: jest.fn(), + fetchContentAssignments: mockFetchContentAssignments, }); useOfferRedemptions.mockReturnValue({ isLoading: false, @@ -323,6 +326,21 @@ describe('', () => { expect(assignedSection.getByText('-$199')).toBeInTheDocument(); expect(assignedSection.getByText('Waiting for learner')).toBeInTheDocument(); expect(assignedSection.getByText(`Assigned: ${formatDate('2023-10-27')}`)).toBeInTheDocument(); + + // Verify "Refresh" behavior + const expectedRefreshArgs = { + pageIndex: DEFAULT_PAGE, + pageSize: PAGE_SIZE, + filters: [], + sortBy: [], + }; + expect(mockFetchContentAssignments).toHaveBeenCalledTimes(1); // called once on initial render + expect(mockFetchContentAssignments).toHaveBeenCalledWith(expect.objectContaining(expectedRefreshArgs)); + const refreshCTA = assignedSection.getByText('Refresh', { selector: 'button' }); + expect(refreshCTA).toBeInTheDocument(); + userEvent.click(refreshCTA); + expect(mockFetchContentAssignments).toHaveBeenCalledTimes(2); // should be called again on refresh + expect(mockFetchContentAssignments).toHaveBeenLastCalledWith(expect.objectContaining(expectedRefreshArgs)); }); it('renders with assigned table data "View Course" hyperlink default when content title is null', () => { diff --git a/src/utils.js b/src/utils.js index 9582955d3c..871af1524a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -400,18 +400,6 @@ const pollAsync = async (pollFunc, timeout, interval, checkFunc) => { return false; }; -const getCourseProductLineText = (courseProductLine) => { - let courseProductLineText = ''; - courseProductLineText = courseProductLine === 'OCM' ? 'Open Courses' : courseProductLine; - return courseProductLineText; -}; - -const getCourseProductLineAbbreviation = (courseProductLine) => { - let courseProductLineText = ''; - courseProductLineText = courseProductLine === 'Open Courses Marketplace' ? 'OCM' : 'Executive Education'; - return courseProductLineText; -}; - export { camelCaseDict, camelCaseDictArray, @@ -445,6 +433,4 @@ export { capitalizeFirstLetter, pollAsync, isNotValidNumberString, - getCourseProductLineText, - getCourseProductLineAbbreviation, }; From 1224e74c998c40a9e48882e1c91c7016d3bce945 Mon Sep 17 00:00:00 2001 From: Mashal Malik <107556986+Mashal-m@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:06:58 +0500 Subject: [PATCH 055/124] refactor: updated README file to reflect template changes (#1075) --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b900a93f7b..10d490cbd5 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@ ![Build Status](https://github.com/openedx/frontend-app-admin-portal/actions/workflows/ci.yml/badge.svg) ![Codecov](https://codecov.io/gh/edx/frontend-app-admin-portal/branch/master/graph/badge.svg) -## Overview +# Purpose frontend-app-admin-portal is a frontend that provides branded learning experiences as well as a dashboard for enterprise learning administrators. +# Getting Started + ## Setting up a dev environment ### The Short Story @@ -101,3 +103,56 @@ module.exports = { ``` NB: In order for webpack to properly resolve scss imports locally, you must use a `~` before the import, like so: `@import "~@edx/brand/paragon/fonts";` + + +## Getting Help + +If you're having trouble, we have discussion forums at +https://discuss.openedx.org where you can connect with others in the community. + +Our real-time conversations are on Slack. You can request a `Slack +invitation`_, then join our `community Slack workspace`_. Because this is a +frontend repository, the best place to discuss it would be in the `#wg-frontend +channel`_. + +For anything non-trivial, the best path is to open an issue in this repository +with as many details about the issue you are facing as you can provide. + +https://github.com/openedx/frontend-app-admin-portal/issues + +For more information about these options, see the `Getting Help`_ page. + +.. _Slack invitation: https://openedx.org/slack +.. _community Slack workspace: https://openedx.slack.com/ +.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6 +.. _Getting Help: https://openedx.org/community/connect + +## Contributing + +Contributions are very welcome. Please read `How To Contribute`_ for details. + +.. _How To Contribute: https://openedx.org/r/how-to-contribute + +This project is currently accepting all types of contributions, bug fixes, +security fixes, maintenance work, or new features. However, please make sure +to have a discussion about your new feature idea with the maintainers prior to +beginning development to maximize the chances of your change being accepted. +You can start a conversation by creating a new issue on this repo summarizing +your idea. + +## The Open edX Code of Conduct + +All community members are expected to follow the `Open edX Code of Conduct`_. + +.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/ + +## License + +The code in this repository is licensed under the AGPLv3 unless otherwise +noted. + +Please see `LICENSE `_ for details. + +## Reporting Security Issues + +Please do not report security issues in public. Please email security@openedx.org. From 6da44f2ff148fff975b4db2c27c7ae0199f2f985 Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan <67655836+alex-sheehan-edx@users.noreply.github.com> Date: Wed, 1 Nov 2023 10:36:11 -0400 Subject: [PATCH 056/124] feat: adding cancel and remind actions to the budget assignment table (#1070) Co-authored-by: Kira Miller Co-authored-by: Kira Miller <31229189+kiram15@users.noreply.github.com> --- .../AssignmentRowActionTableCell.jsx | 64 ++++++++++ .../AssignmentTableCancel.jsx | 21 ++++ .../AssignmentTableRemind.jsx | 29 +++++ .../BudgetAssignmentsTable.jsx | 16 ++- .../tests/BudgetDetailPage.test.jsx | 113 ++++++++++++++++++ 5 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/components/learner-credit-management/AssignmentRowActionTableCell.jsx create mode 100644 src/components/learner-credit-management/AssignmentTableCancel.jsx create mode 100644 src/components/learner-credit-management/AssignmentTableRemind.jsx diff --git a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx new file mode 100644 index 0000000000..1d771c27fa --- /dev/null +++ b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Icon, + IconButton, + OverlayTrigger, + Stack, + Tooltip, +} from '@edx/paragon'; +import { Mail, DoNotDisturbOn } from '@edx/paragon/icons'; + +const AssignmentRowActionTableCell = ({ row }) => { + const cancelButtonMarginLeft = row.original.state === 'allocated' ? 'ml-2.5' : 'ml-auto'; + return ( +
+ {row.original.state === 'allocated' && ( + <> + Remind learner} + > + console.log(`Reminding ${row.original.uuid}`)} + data-testid={`remind-assignment-${row.original.uuid}`} + /> + + + + )} + Cancel assignment} + > + console.log(`Canceling ${row.original.uuid}`)} + data-testid={`cancel-assignment-${row.original.uuid}`} + /> + +
+ ); +}; + +AssignmentRowActionTableCell.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + uuid: PropTypes.string.isRequired, + state: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; + +export default AssignmentRowActionTableCell; diff --git a/src/components/learner-credit-management/AssignmentTableCancel.jsx b/src/components/learner-credit-management/AssignmentTableCancel.jsx new file mode 100644 index 0000000000..37c3e8ef6d --- /dev/null +++ b/src/components/learner-credit-management/AssignmentTableCancel.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@edx/paragon'; +import { DoNotDisturbOn } from '@edx/paragon/icons'; + +const AssignmentTableCancelAction = ({ selectedFlatRows, ...rest }) => ( + // eslint-disable-next-line no-console + +); + +AssignmentTableCancelAction.propTypes = { + selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), +}; + +AssignmentTableCancelAction.defaultProps = { + selectedFlatRows: [], +}; + +export default AssignmentTableCancelAction; diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx new file mode 100644 index 0000000000..bee8cc9814 --- /dev/null +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@edx/paragon'; +import { Mail } from '@edx/paragon/icons'; + +const AssignmentTableRemindAction = ({ selectedFlatRows, ...rest }) => { + const hideRemindAction = selectedFlatRows.some( + row => row.original.state !== 'allocated', + ); + if (hideRemindAction) { + return null; + } + return ( + // eslint-disable-next-line no-console + + ); +}; + +AssignmentTableRemindAction.propTypes = { + selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), +}; + +AssignmentTableRemindAction.defaultProps = { + selectedFlatRows: [], +}; + +export default AssignmentTableRemindAction; diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index 301dfc99f8..cfc6603800 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -1,11 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { DataTable } from '@edx/paragon'; - import TableTextFilter from './TableTextFilter'; import CustomDataTableEmptyState from './CustomDataTableEmptyState'; import AssignmentDetailsTableCell from './AssignmentDetailsTableCell'; import AssignmentStatusTableCell from './AssignmentStatusTableCell'; +import AssignmentRowActionTableCell from './AssignmentRowActionTableCell'; +import AssignmentTableRemindAction from './AssignmentTableRemind'; +import AssignmentTableCancelAction from './AssignmentTableCancel'; import { DEFAULT_PAGE, PAGE_SIZE, formatPrice } from './data'; import AssignmentRecentActionTableCell from './AssignmentRecentActionTableCell'; import AssignmentsTableRefreshAction from './AssignmentsTableRefreshAction'; @@ -19,6 +21,7 @@ const BudgetAssignmentsTable = ({ }) => ( , ]} @@ -68,6 +78,10 @@ const BudgetAssignmentsTable = ({ itemCount={tableData?.count || 0} pageCount={tableData?.numPages || 1} EmptyTableComponent={CustomDataTableEmptyState} + bulkActions={[ + , + , + ]} /> ); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 3c5574ec22..462b32d46c 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -723,4 +723,117 @@ describe('', () => { expect(screen.getByText('loading budget activity overview')).toBeInTheDocument(); }); + + it('displays remind row and bulk actions when allocated', async () => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [ + { + uuid: 'test-uuid', + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState: 'active', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [], + state: 'allocated', + }, + ], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + renderWithRouter(); + const cancelRowAction = screen.getByTestId('cancel-assignment-test-uuid'); + const remindRowAction = screen.getByTestId('remind-assignment-test-uuid'); + expect(cancelRowAction).toBeInTheDocument(); + expect(remindRowAction).toBeInTheDocument(); + + const checkBox = screen.getByTestId('datatable-select-column-checkbox-cell'); + expect(checkBox).toBeInTheDocument(); + userEvent.click(checkBox); + await waitFor(() => { + expect(screen.getByText('Remind (1)')).toBeInTheDocument(); + }); + await waitFor(() => { + expect(screen.getByText('Cancel (1)')).toBeInTheDocument(); + }); + }); + + it('hides remind row and bulk actions when allocated', () => { + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [ + { + uuid: 'test-uuid', + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState: 'accepted', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [], + state: 'accepted', + }, + ], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + renderWithRouter(); + expect(screen.queryByTestId('remind-assignment-test-uuid')).not.toBeInTheDocument(); + const checkBox = screen.getByTestId('datatable-select-column-checkbox-cell'); + userEvent.click(checkBox); + expect(screen.queryByText('Remind (1)')).not.toBeInTheDocument(); + }); }); From 17f1d7df77d0b6c42c2ca174950a901d20794625 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Wed, 1 Nov 2023 11:26:29 -0400 Subject: [PATCH 057/124] feat: add assignment modal empty state (#1078) --- __mocks__/react-instantsearch-dom.jsx | 9 +- .../EnterpriseApp/EnterpriseAppRoutes.jsx | 1 + src/components/EnterpriseApp/index.jsx | 6 +- .../BudgetDetailPage.jsx | 2 + .../BudgetDetailTabsAndRoutes.jsx | 3 + .../WaitingForLearner.jsx | 4 +- .../cards/AssignmentModalContent.jsx | 83 ++++++++ .../cards/BaseCourseCard.jsx | 101 +++++++++ .../cards/Collapsibles.jsx | 76 +++++++ .../cards/CourseCard.jsx | 173 +++------------- .../cards/CourseCard.test.jsx | 195 ++++++++++++++---- .../cards/NewAssignmentModalButton.jsx | 46 +++++ .../cards/data/useCourseCardMetadata.js | 73 +++++++ .../learner-credit-management/data/utils.js | 6 +- .../learner-credit-management/index.jsx | 4 +- .../search/CatalogSearchResults.jsx | 1 + .../styles/index.scss | 45 ++-- .../tests/CatalogSearchResults.test.jsx | 12 ++ 18 files changed, 633 insertions(+), 207 deletions(-) create mode 100644 src/components/learner-credit-management/cards/AssignmentModalContent.jsx create mode 100644 src/components/learner-credit-management/cards/BaseCourseCard.jsx create mode 100644 src/components/learner-credit-management/cards/Collapsibles.jsx create mode 100644 src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx create mode 100644 src/components/learner-credit-management/cards/data/useCourseCardMetadata.js diff --git a/__mocks__/react-instantsearch-dom.jsx b/__mocks__/react-instantsearch-dom.jsx index 4444b52c33..dbb7fbaba4 100644 --- a/__mocks__/react-instantsearch-dom.jsx +++ b/__mocks__/react-instantsearch-dom.jsx @@ -12,11 +12,16 @@ const advertised_course_run = { start: '2020-09-09T04:00:00Z', key: 'course-v1:edX+Bee101+3T2020', }; +const mockNormalizedData = { + start_date: '2020-09-09T04:00:00Z', + end_date: '2021-09-09T04:00:00Z', + enroll_by_date: '2020-09-15T04:00:00Z', +}; /* eslint-disable camelcase */ const fakeHits = [ - { objectID: '1', aggregation_key: 'course:Bees101', title: 'bla', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Bees101' }, - { objectID: '2', aggregation_key: 'course:Wasps200', title: 'blp', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Wasps200' }, + { objectID: '1', aggregation_key: 'course:Bees101', title: 'bla', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Bees101', normalized_metadata: mockNormalizedData }, + { objectID: '2', aggregation_key: 'course:Wasps200', title: 'blp', partners: [{ name: 'edX' }, { name: 'another_unused' }], advertised_course_run, key: 'Wasps200', normalized_metadata: mockNormalizedData }, ]; /* eslint-enable camelcase */ diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx index 07b10958cb..2393795907 100644 --- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx +++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx @@ -29,6 +29,7 @@ const EnterpriseAppRoutes = ({ enableContentHighlightsPage, }) => { const { canManageLearnerCredit } = useContext(EnterpriseSubsidiesContext); + console.log('EnterpriseAppRoutes!!!'); return ( { data: subsidyAccessPolicy, } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + console.log('BudgetDetailPage!!!'); + if (isInitialLoadingSubsidyAccessPolicy) { return ( diff --git a/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx b/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx index 1d0ef0db7f..732682a35c 100644 --- a/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx +++ b/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx @@ -43,6 +43,9 @@ const BudgetDetailTabsAndRoutes = ({ enterpriseSlug, enterpriseFeatures, }) => { + + console.log('BudgetDetailTabsAndRoutes!!!'); + const { activeTabKey: routeActiveTabKey } = useParams(); const { budgetId, subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); diff --git a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx index 1930262097..50a06dc522 100644 --- a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx @@ -4,6 +4,7 @@ import { Chip, Hyperlink, useToggle } from '@edx/paragon'; import { Timelapse } from '@edx/paragon/icons'; import BaseModalPopup from './BaseModalPopup'; +import { ASSIGNMENT_ENROLLMENT_DEADLINE } from '../data'; const WaitingForLearner = ({ learnerEmail }) => { const [isOpen, open, close] = useToggle(false); @@ -31,7 +32,8 @@ const WaitingForLearner = ({ learnerEmail }) => {

This learner must create an edX account and complete enrollment in the course before the - enrollment deadline or within 90 days of assignment, whichever is sooner. + enrollment deadline or within {ASSIGNMENT_ENROLLMENT_DEADLINE} days of assignment, whichever + is sooner.

Need help?

diff --git a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx new file mode 100644 index 0000000000..65bfd3e672 --- /dev/null +++ b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { + Container, + Stack, + Row, + Col, + Form, + Card, +} from '@edx/paragon'; + +import BaseCourseCard from './BaseCourseCard'; +import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from '../data'; +import { ImpactOnYourLearnerCreditBudget, ManagingThisAssignment, NextStepsForAssignedLearners } from './Collapsibles'; + +const AssignmentModalContent = ({ course }) => { + const [emailAddresses, setEmailAddresses] = useState(''); + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + return ( + + + + +

Use Learner Credit to assign this course

+ + +
+ + +

Assign to

+ + setEmailAddresses(e.target.value)} + floatingLabel="Learner email addresses" + rows={10} + data-hj-suppress + /> + + To add more than one learner, enter one email address per line. + + +
How assigning this course works
+ + + + + + + +

Pay by Learner Credit

+
Summary
+ + +
You haven't entered any learners yet.
+ Add learner emails to get started. +
+
+
+
+ Learner Credit Budget: {subsidyAccessPolicy.displayName ?? 'Overview'} +
+ + +
Available balance
+
{formatPrice(subsidyAccessPolicy.aggregates.spendAvailableUsd)}
+
+
+ +
+
+
+ ); +}; + +AssignmentModalContent.propTypes = { + course: PropTypes.shape().isRequired, // Pass-thru prop to `BaseCourseCard` +}; + +export default AssignmentModalContent; diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx new file mode 100644 index 0000000000..ef81a4f620 --- /dev/null +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { + useMediaQuery, + breakpoints, + Card, + Stack, + Badge, +} from '@edx/paragon'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; + +import useCourseCardMetadata from './data/useCourseCardMetadata'; +import CARD_TEXT from '../constants'; + +const BaseCourseCard = ({ + original, + footerActions: CardFooterActions, + enterpriseSlug, + cardClassName, +}) => { + const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); + const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); + const courseCardMetadata = useCourseCardMetadata({ + course: camelCaseObject(original), + enterpriseSlug, + }); + const { + imageSrc, + altText, + logoSrc, + logoAlt, + title, + subtitle, + price, + isExecEdCourseType, + courseEnrollmentInfo, + execEdEnrollmentInfo, + } = courseCardMetadata; + const { BADGE, PRICE } = CARD_TEXT; + + return ( + + + + +
{price}
+ {PRICE.subText} + + )} + /> + + + {isExecEdCourseType ? BADGE.execEd : BADGE.course} + + + + {CardFooterActions && } + +
+
+ ); +}; + +const mapStateToProps = state => ({ + enterpriseSlug: state.portalConfiguration.enterpriseSlug, +}); + +BaseCourseCard.propTypes = { + enterpriseSlug: PropTypes.string.isRequired, + original: PropTypes.shape({ + availability: PropTypes.arrayOf(PropTypes.string), + cardImageUrl: PropTypes.string, + courseType: PropTypes.string, + normalizedMetadata: PropTypes.shape(), + originalImageUrl: PropTypes.string, + partners: PropTypes.arrayOf( + PropTypes.shape({ + logoImageUrl: PropTypes.string, + name: PropTypes.string, + }), + ), + title: PropTypes.string, + }).isRequired, + footerActions: PropTypes.elementType, + cardClassName: PropTypes.string, +}; + +export default connect(mapStateToProps)(BaseCourseCard); diff --git a/src/components/learner-credit-management/cards/Collapsibles.jsx b/src/components/learner-credit-management/cards/Collapsibles.jsx new file mode 100644 index 0000000000..12d82a6be2 --- /dev/null +++ b/src/components/learner-credit-management/cards/Collapsibles.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Collapsible } from '@edx/paragon'; + +import { ASSIGNMENT_ENROLLMENT_DEADLINE } from '../data'; + +export const NextStepsForAssignedLearners = ({ course }) => ( + Next steps for assigned learners} + defaultOpen + > +
+
    +
  • + Learners will be notified of this course assignment by email. +
  • +
  • + Learners must complete enrollment for this assignment by {course.enrollmentDeadline}. This deadline + is calculated based on the course enrollment deadline or {ASSIGNMENT_ENROLLMENT_DEADLINE} days + past the date of assignment, whichever is sooner. +
  • +
  • + Learners will receive automated reminder emails every 10-15 days until the enrollment + deadline is reached. +
  • +
+
+
+); + +NextStepsForAssignedLearners.propTypes = { + course: PropTypes.shape({ + enrollmentDeadline: PropTypes.string.isRequired, + }).isRequired, +}; + +export const ImpactOnYourLearnerCreditBudget = () => ( + Impact on your Learner Credit budget} + > +
+
    +
  • + The total assignment cost will be earmarked as "assigned" funds in your + Learner Credit budget so you can't overspend. +
  • +
  • + The course cost will automatically convert from "assigned" to "spent" funds + when your learners complete registration. +
  • +
+
+
+); + +export const ManagingThisAssignment = () => ( + Managing this assignment} + > +
+
    +
  • + You will be able to monitor the status of this assignment by reviewing + your Learner Credit Budget activity. +
  • +
  • + You can cancel this course assignment or send email reminders any time + before learners complete enrollment. +
  • +
+
+
+); diff --git a/src/components/learner-credit-management/cards/CourseCard.jsx b/src/components/learner-credit-management/cards/CourseCard.jsx index 2d98bf20eb..df2e7379d8 100644 --- a/src/components/learner-credit-management/cards/CourseCard.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.jsx @@ -1,155 +1,42 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -// variables taken from algolia not in camelcase -import React, { useContext } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { AppContext } from '@edx/frontend-platform/react'; -import { connect } from 'react-redux'; +import { Button, Hyperlink } from '@edx/paragon'; -import { - Badge, - Button, - Card, - Stack, - Hyperlink, - useMediaQuery, - breakpoints, -} from '@edx/paragon'; -import { injectIntl } from '@edx/frontend-platform/i18n'; -import { camelCaseObject } from '@edx/frontend-platform'; -import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; - -import { EXEC_ED_COURSE_TYPE } from '../data'; -import { formatPrice, formatDate, getEnrollmentDeadline } from '../data/utils'; +import NewAssignmentModalButton from './NewAssignmentModalButton'; import CARD_TEXT from '../constants'; +import BaseCourseCard from './BaseCourseCard'; -const CourseCard = ({ - original, enterpriseSlug, -}) => { - const { - availability, - cardImageUrl, - courseType, - key, - normalizedMetadata, - partners, - title, - } = camelCaseObject(original); - - const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext); - const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); - const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); - - const { - BADGE, - BUTTON_ACTION, - PRICE, - ENROLLMENT, - } = CARD_TEXT; - - const price = normalizedMetadata?.contentPrice ? formatPrice(normalizedMetadata.contentPrice, { minimumFractionDigits: 0 }) : 'N/A'; - - const imageSrc = cardImageUrl || cardFallbackImg; - - let logoSrc; - let logoAlt; - if (partners.length === 1) { - logoSrc = partners[0]?.logoImageUrl; - logoAlt = `${partners[0]?.name}'s logo`; - } - - const altText = `${title} course image`; +const { BUTTON_ACTION } = CARD_TEXT; - const formattedAvailability = availability?.length ? availability.join(', ') : null; +const CourseCardFooterActions = (course) => { + const { linkToCourse } = course; - const enrollmentDeadline = getEnrollmentDeadline(normalizedMetadata?.enrollByDate); - - let courseEnrollmentInfo; - let execEdEnrollmentInfo; - if (normalizedMetadata?.enrollByDate) { - courseEnrollmentInfo = `${formattedAvailability} • ${ENROLLMENT.text} ${enrollmentDeadline}`; - execEdEnrollmentInfo = `Starts ${formatDate(normalizedMetadata.startDate)} • - ${ENROLLMENT.text} ${enrollmentDeadline}`; - } else { - courseEnrollmentInfo = formattedAvailability; - execEdEnrollmentInfo = formattedAvailability; - } - - const isExecEd = courseType === EXEC_ED_COURSE_TYPE; - - let linkToCourse; - if (isExecEd) { - linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`; - } else { - linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${key}`; - } - - return ( - - - - -

{price}

- {PRICE.subText} - - )} - /> - - - {isExecEd ? BADGE.execEd : BADGE.course} - - - - - - -
-
- ); + {BUTTON_ACTION.viewCourse} + , + + {BUTTON_ACTION.assign} + , + ]; }; +const CourseCard = ({ original }) => ( + +); + CourseCard.propTypes = { - enterpriseSlug: PropTypes.string.isRequired, - original: PropTypes.shape({ - availability: PropTypes.arrayOf(PropTypes.string), - cardImageUrl: PropTypes.string, - courseType: PropTypes.string, - normalizedMetadata: PropTypes.shape(), - originalImageUrl: PropTypes.string, - partners: PropTypes.arrayOf( - PropTypes.shape({ - logoImageUrl: PropTypes.string, - name: PropTypes.string, - }), - ), - title: PropTypes.string, - }).isRequired, + original: PropTypes.shape().isRequired, // pass-thru prop to `BaseCourseCard` }; -const mapStateToProps = state => ({ - enterpriseSlug: state.portalConfiguration.enterpriseSlug, -}); - -export default connect(mapStateToProps)(injectIntl(CourseCard)); +export default CourseCard; diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/CourseCard.test.jsx index cb9304032a..be225b0c9a 100644 --- a/src/components/learner-credit-management/cards/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.test.jsx @@ -1,12 +1,17 @@ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AppContext } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { renderWithRouter } from '@edx/frontend-enterprise-utils'; + import CourseCard from './CourseCard'; +import { formatPrice, useSubsidyAccessPolicy } from '../data'; const originalData = { availability: ['Upcoming'], @@ -22,6 +27,7 @@ const originalData = { partners: [{ logo_image_url: '', name: 'Course Provider' }], title: 'Course Title', }; +const imageAltText = `${originalData.title} course image`; const defaultProps = { original: originalData, @@ -60,6 +66,27 @@ const initialStoreState = { }, }; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const mockSubsidyAccessPolicy = { + uuid: 'test-subsidy-access-policy-uuid', + displayName: 'Test Subsidy Access Policy', + aggregates: { + spendAvailableUsd: 50000, + }, +}; + +jest.mock('../data', () => ({ + ...jest.requireActual('../data'), + useSubsidyAccessPolicy: jest.fn(), +})); + const CourseCardWrapper = ({ initialState = initialStoreState, ...rest @@ -67,54 +94,146 @@ const CourseCardWrapper = ({ const store = getMockStore({ ...initialState }); return ( - - - - - - - + + + + + + + + + ); }; describe('Course card works as expected', () => { - test('course card renders', () => { - render(); - expect(screen.queryByText(defaultProps.original.title)).toBeInTheDocument(); - expect( - screen.queryByText(defaultProps.original.partners[0].name), - ).toBeInTheDocument(); - expect(screen.queryByText('$100')).toBeInTheDocument(); - expect(screen.queryByText('Per learner price')).toBeInTheDocument(); - expect(screen.queryByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); - expect(screen.queryByText('Course')).toBeInTheDocument(); - expect(screen.queryByText('View course')).toBeInTheDocument(); - expect(screen.queryByText('Assign')).toBeInTheDocument(); - const hyperlink = screen.getByRole('link', { - name: 'View course Opens in a new tab', + beforeEach(() => { + useSubsidyAccessPolicy.mockReturnValue({ + data: mockSubsidyAccessPolicy, + isLoading: false, }); - expect(hyperlink.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/course/course-123x'); }); - test('test card renders default image', async () => { - render(); - const imageAltText = `${originalData.title} course image`; - fireEvent.error(screen.getByAltText(imageAltText)); - await expect(screen.getByAltText(imageAltText).src).not.toBeUndefined; + afterEach(() => { + jest.clearAllMocks(); + }); + + test('course card renders', () => { + renderWithRouter(); + expect(screen.getByText(defaultProps.original.title)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.original.partners[0].name)).toBeInTheDocument(); + expect(screen.getByText('$100')).toBeInTheDocument(); + expect(screen.getByText('Per learner price')).toBeInTheDocument(); + expect(screen.getByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); + expect(screen.getByText('Course')).toBeInTheDocument(); + // Has card image defined even though the course metadata does not contain an image URL + const cardImage = screen.getByAltText(imageAltText); + expect(cardImage).toBeInTheDocument(); + expect(cardImage.src).toBeDefined(); + + // Footer actions + const viewCourseCTA = screen.getByText('View course', { selector: 'a' }); + expect(viewCourseCTA).toBeInTheDocument(); + expect(viewCourseCTA.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/course/course-123x'); + const assignCourseCTA = screen.getByText('Assign', { selector: 'button' }); + expect(assignCourseCTA).toBeInTheDocument(); }); - test('exec ed card renders', async () => { - render(); + test('card renders given image', () => { + const mockCardImageUrl = 'https://example.com/image.jpg'; + const props = { + ...defaultProps, + original: { + ...originalData, + card_image_url: mockCardImageUrl, + }, + }; + renderWithRouter(); + const cardImage = screen.getByAltText(imageAltText); + expect(cardImage).toBeInTheDocument(); + expect(cardImage.src).toEqual(mockCardImageUrl); + }); + + test('executive education card renders', () => { + renderWithRouter(); expect(screen.queryByText('$999')).toBeInTheDocument(); expect(screen.queryByText('Starts Apr 18, 2016 • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); expect(screen.queryByText('Executive Education')).toBeInTheDocument(); - const hyperlink = screen.getByRole('link', { - name: 'View course Opens in a new tab', - }); - expect(hyperlink.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/executive-education-2u/course/exec-ed-course-123x'); + const viewCourseCTA = screen.getByText('View course', { selector: 'a' }); + expect(viewCourseCTA.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/executive-education-2u/course/exec-ed-course-123x'); + }); + + test('opens assignment modal', () => { + renderWithRouter(); + const assignCourseCTA = screen.getByText('Assign', { selector: 'button' }); + expect(assignCourseCTA).toBeInTheDocument(); + userEvent.click(assignCourseCTA); + + const assignmentModal = within(screen.getByRole('dialog')); + + expect(assignmentModal.getByText('Assign this course')).toBeInTheDocument(); + expect(assignmentModal.getByText('Use Learner Credit to assign this course')).toBeInTheDocument(); + + // Verify course card is displayed WITHOUT footer actions + const modalCourseCard = within(assignmentModal.getByText('Course Title').closest('.pgn__card')); + expect(modalCourseCard.getByText(defaultProps.original.title)).toBeInTheDocument(); + expect(modalCourseCard.getByText(defaultProps.original.partners[0].name)).toBeInTheDocument(); + expect(modalCourseCard.getByText('$100')).toBeInTheDocument(); + expect(modalCourseCard.getByText('Per learner price')).toBeInTheDocument(); + expect(modalCourseCard.getByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument(); + expect(modalCourseCard.getByText('Course')).toBeInTheDocument(); + const cardImage = modalCourseCard.getByAltText(imageAltText); + expect(cardImage).toBeInTheDocument(); + expect(cardImage.src).toBeDefined(); + expect(modalCourseCard.queryByText('View course', { selector: 'a' })).not.toBeInTheDocument(); + expect(modalCourseCard.queryByText('Assign', { selector: 'button' })).not.toBeInTheDocument(); + + // Verify empty state and textarea can accept emails + expect(assignmentModal.getByText('Assign to')).toBeInTheDocument(); + const textareaInputLabel = assignmentModal.getByLabelText('Learner email addresses'); + expect(textareaInputLabel).toBeInTheDocument(); + const textareaInput = textareaInputLabel.closest('textarea'); + expect(textareaInput).toBeInTheDocument(); + userEvent.type(textareaInput, 'hello@example.com{enter}world@example.com'); + expect(textareaInput).toHaveValue('hello@example.com\nworld@example.com'); + expect(assignmentModal.getByText('To add more than one learner, enter one email address per line.')).toBeInTheDocument(); + expect(assignmentModal.getByText('Pay by Learner Credit')).toBeInTheDocument(); + expect(assignmentModal.getByText('Summary')).toBeInTheDocument(); + expect(assignmentModal.getByText('You haven\'t entered any learners yet.')).toBeInTheDocument(); + expect(assignmentModal.getByText('Add learner emails to get started.')).toBeInTheDocument(); + expect(assignmentModal.getByText(`Learner Credit Budget: ${mockSubsidyAccessPolicy.displayName}`)).toBeInTheDocument(); + expect(assignmentModal.getByText('Available balance')).toBeInTheDocument(); + const expectedAvailableBalance = formatPrice(mockSubsidyAccessPolicy.aggregates.spendAvailableUsd); + expect(assignmentModal.getByText(expectedAvailableBalance)).toBeInTheDocument(); + + // Verify collapsibles + expect(assignmentModal.getByText('How assigning this course works')).toBeInTheDocument(); + expect(assignmentModal.getByText('Next steps for assigned learners')).toBeInTheDocument(); + expect(assignmentModal.getByText('Learners will be notified of this course assignment by email.')).toBeInTheDocument(); + const budgetImpact = assignmentModal.getByText('Impact on your Learner Credit budget'); + expect(budgetImpact).toBeInTheDocument(); + expect(assignmentModal.queryByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).not.toBeInTheDocument(); + userEvent.click(budgetImpact); + expect(assignmentModal.getByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).toBeInTheDocument(); + const managingAssignment = assignmentModal.getByText('Managing this assignment'); + expect(managingAssignment).toBeInTheDocument(); + expect(assignmentModal.queryByText('You will be able to monitor the status of this assignment', { exact: false })).not.toBeInTheDocument(); + userEvent.click(managingAssignment); + expect(assignmentModal.getByText('You will be able to monitor the status of this assignment', { exact: false })).toBeInTheDocument(); + + // Verify modal footer + expect(assignmentModal.getByText('Help Center: Course Assignments')).toBeInTheDocument(); + const cancelAssignmentCTA = assignmentModal.getByText('Cancel', { selector: 'button' }); + expect(cancelAssignmentCTA).toBeInTheDocument(); + const submitAssignmentCTA = assignmentModal.getByText('Assign', { selector: 'button' }); + expect(submitAssignmentCTA).toBeInTheDocument(); + + // Verify modal closes + userEvent.click(cancelAssignmentCTA); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); }); diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx new file mode 100644 index 0000000000..1b63968106 --- /dev/null +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + FullscreenModal, + ActionRow, + Button, + useToggle, + Hyperlink, +} from '@edx/paragon'; +import AssignmentModalContent from './AssignmentModalContent'; + +const NewAssignmentModalButton = ({ course, children }) => { + const [isOpen, open, close] = useToggle(false); + + return ( + <> + + + + + + {/* TODO: https://2u-internal.atlassian.net/browse/ENT-7826 */} + + + )} + > + + + + ); +}; + +NewAssignmentModalButton.propTypes = { + course: PropTypes.shape().isRequired, // Pass-thru prop to `BaseCourseCard` + children: PropTypes.node.isRequired, // Represents the button text +}; + +export default NewAssignmentModalButton; diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js new file mode 100644 index 0000000000..cfb4312e20 --- /dev/null +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js @@ -0,0 +1,73 @@ +import { useContext } from 'react'; +import { AppContext } from '@edx/frontend-platform/react'; +import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; + +import CARD_TEXT from '../../constants'; +import { + EXEC_ED_COURSE_TYPE, + formatDate, + formatPrice, + getEnrollmentDeadline, +} from '../../data'; + +const { ENROLLMENT } = CARD_TEXT; + +const useCourseCardMetadata = ({ + course, + enterpriseSlug, +}) => { + const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext); + const { + availability, + cardImageUrl, + courseType, + key, + normalizedMetadata, + partners, + title, + } = course; + const price = (normalizedMetadata.contentPrice || normalizedMetadata.contentPrice === 0) ? formatPrice(normalizedMetadata.contentPrice) : 'N/A'; + const imageSrc = cardImageUrl || cardFallbackImg; + + let logoSrc; + let logoAlt; + if (partners.length === 1) { + logoSrc = partners[0]?.logoImageUrl; + logoAlt = `${partners[0]?.name}'s logo`; + } + + const altText = `${title} course image`; + const formattedAvailability = availability?.length ? availability.join(', ') : null; + const enrollmentDeadline = getEnrollmentDeadline(normalizedMetadata.enrollByDate); + + let courseEnrollmentInfo = ''; + if (formattedAvailability) { + courseEnrollmentInfo = `${formattedAvailability} • `; + } + courseEnrollmentInfo += `${ENROLLMENT.text} ${enrollmentDeadline}`; + const execEdEnrollmentInfo = `Starts ${formatDate(normalizedMetadata.startDate)} • ${ENROLLMENT.text} ${enrollmentDeadline}`; + + const isExecEdCourseType = courseType === EXEC_ED_COURSE_TYPE; + + let linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${key}`; + if (isExecEdCourseType) { + linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`; + } + + return { + ...course, + subtitle: partners.map(partner => partner.name).join(', '), + price, + imageSrc, + altText, + logoSrc, + logoAlt, + enrollmentDeadline, + courseEnrollmentInfo, + execEdEnrollmentInfo, + linkToCourse, + isExecEdCourseType, + }; +}; + +export default useCourseCardMetadata; diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 6a5a45888c..61122ad402 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -203,9 +203,11 @@ export function formatDate(date) { // Exec ed and open courses cards should display either the enrollment deadline // or 90 days from the present date on user pageload, whichever is sooner. export function getEnrollmentDeadline(enrollByDate) { - const courseEnrollByDate = dayjs(enrollByDate); const assignmentEnrollmentDeadline = dayjs().add(ASSIGNMENT_ENROLLMENT_DEADLINE, 'days'); - + if (!enrollByDate) { + return formatDate(assignmentEnrollmentDeadline); + } + const courseEnrollByDate = dayjs(enrollByDate); return courseEnrollByDate <= assignmentEnrollmentDeadline ? formatDate(courseEnrollByDate) : formatDate(assignmentEnrollmentDeadline); diff --git a/src/components/learner-credit-management/index.jsx b/src/components/learner-credit-management/index.jsx index 43786e8a0b..0eaef07494 100644 --- a/src/components/learner-credit-management/index.jsx +++ b/src/components/learner-credit-management/index.jsx @@ -5,7 +5,7 @@ import MultipleBudgetsPage from './MultipleBudgetsPage'; import BudgetDetailPage from './BudgetDetailPage'; const LearnerCreditManagementRoutes = ({ match }) => ( - <> +
( path={`${match.path}/:budgetId/:activeTabKey?`} component={BudgetDetailPage} /> - +
); LearnerCreditManagementRoutes.propTypes = { diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx index fdd9bde51b..a256e2f5e7 100644 --- a/src/components/learner-credit-management/search/CatalogSearchResults.jsx +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -33,6 +33,7 @@ export const BaseCatalogSearchResults = ({ error, setNoContent, }) => { + console.log('BaseCatalogSearchResults!!!', searchResults); const courseColumns = useMemo( () => [ { diff --git a/src/components/learner-credit-management/styles/index.scss b/src/components/learner-credit-management/styles/index.scss index c3301e7af2..3db792b691 100644 --- a/src/components/learner-credit-management/styles/index.scss +++ b/src/components/learner-credit-management/styles/index.scss @@ -1,22 +1,31 @@ -.budget-detail-assignments { - // This is a (temporary) workaround to ensure the `Chip` modal popups within the "Assigned" table status column - // properly overlays the underlying `DataTable`. - .pgn__data-table-container, - .pgn__data-table-layout-wrapper { - overflow-x: visible; - } - - // The `Card` component in Paragon does not seem to properly let consumers customize the width of the `Card.Body` - // contents when in the horizontal card orientation without custom CSS. As a result, both the `Card.Footer` and - // `Card.Body` incorrectly get equal column widths when the preference is that the `Card.Body` has more width than - // the `Card.Footer`. The below styles force the `Card.Body` to have appropriately more width than the `Card.Footer` when - // the `Card` is in the horizontal orientation. - .assign-more-courses-empty-state-minimal { - .assign-more-courses__card-body { - flex: 3; +.learner-credit-management { + .budget-detail-assignments { + // This is a (temporary) workaround to ensure the `Chip` modal popups within the "Assigned" table status column + // properly overlays the underlying `DataTable`. + .pgn__data-table-container, + .pgn__data-table-layout-wrapper { + overflow-x: visible; } - .assign-more-courses__card-footer { - flex: 1; + + // The `Card` component in Paragon does not seem to properly let consumers customize the width of the `Card.Body` + // contents when in the horizontal card orientation without custom CSS. As a result, both the `Card.Footer` and + // `Card.Body` incorrectly get equal column widths when the preference is that the `Card.Body` has more width than + // the `Card.Footer`. The below styles (temporary) force the `Card.Body` to have appropriately more width than the + // `Card.Footer` when the `Card` is in the horizontal orientation. + .assign-more-courses-empty-state-minimal { + .assign-more-courses__card-body { + flex: 3; + } + .assign-more-courses__card-footer { + flex: 1; + } } } } + + +// Must be defined outside of `.learner-credit-management` to ensure the styles are applied to the contents of +// the `FullscreenModal`, which renders in a React Portal. +.assignment-modal-collapsible-trigger { + text-decoration: underline; +} diff --git a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx index 503833aa57..b26b2de7ec 100644 --- a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx @@ -88,6 +88,12 @@ const searchResults = { upgrade_deadline: 1892678399, pacing_type: 'self_paced', }, + normalized_metadata: { + start_date: '2020-09-09T04:00:00Z', + end_date: '2021-09-09T04:00:00Z', + enroll_by_date: '2020-09-15T04:00:00Z', + content_price: 199, + }, }, { title: TEST_COURSE_NAME_2, @@ -104,6 +110,12 @@ const searchResults = { upgrade_deadline: 1892678399, pacing_type: 'self_paced', }, + normalized_metadata: { + start_date: '2020-09-09T04:00:00Z', + end_date: '2021-09-09T04:00:00Z', + enroll_by_date: '2020-09-15T04:00:00Z', + content_price: 199, + }, }, ], page: 1, From 8211761bdce0608d21e01ee2d48ff589315e681a Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Thu, 2 Nov 2023 11:23:38 -0400 Subject: [PATCH 058/124] fix: rely on new `error_reason` field to determine failed `learner_state` reason (#1079) --- .../EnterpriseApp/EnterpriseAppRoutes.jsx | 1 - src/components/EnterpriseApp/index.jsx | 6 +--- .../AssignmentStatusTableCell.jsx | 17 ++++++----- .../BudgetDetailPage.jsx | 2 -- .../BudgetDetailTabsAndRoutes.jsx | 3 -- .../cards/Collapsibles.jsx | 4 --- .../search/CatalogSearchResults.jsx | 1 - .../tests/BudgetDetailPage.test.jsx | 30 ++++++++++++++----- 8 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx index 2393795907..07b10958cb 100644 --- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx +++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx @@ -29,7 +29,6 @@ const EnterpriseAppRoutes = ({ enableContentHighlightsPage, }) => { const { canManageLearnerCredit } = useContext(EnterpriseSubsidiesContext); - console.log('EnterpriseAppRoutes!!!'); return ( { - const { original: { learnerEmail, learnerState } } = row; + const { original } = row; + const { + learnerEmail, + learnerState, + errorReason, + } = original; // Learner state is not available for this assignment, so don't display anything. if (!learnerState) { return null; } + // Display the appropriate status chip based on the learner state. if (learnerState === 'notifying') { return ( @@ -29,12 +35,8 @@ const AssignmentStatusTableCell = ({ row }) => { } if (learnerState === 'failed') { - // Determine the failure reason based on the actions. - const { actions } = row.original; - const mostRecentAction = actions[0]; // API returns actions in reverse chronological order. - const isBadEmailError = mostRecentAction.actionType === 'notified' && !!mostRecentAction.errorReason; - - if (isBadEmailError) { + // Determine which failure chip to display based on the error reason. + if (errorReason === 'email_error') { return ( ); @@ -52,6 +54,7 @@ AssignmentStatusTableCell.propTypes = { original: PropTypes.shape({ learnerEmail: PropTypes.string, learnerState: PropTypes.string.isRequired, + errorReason: PropTypes.string, actions: PropTypes.arrayOf(PropTypes.shape({ actionType: PropTypes.string.isRequired, errorReason: PropTypes.string, diff --git a/src/components/learner-credit-management/BudgetDetailPage.jsx b/src/components/learner-credit-management/BudgetDetailPage.jsx index 4591c9eee3..1ad1ff56cc 100644 --- a/src/components/learner-credit-management/BudgetDetailPage.jsx +++ b/src/components/learner-credit-management/BudgetDetailPage.jsx @@ -13,8 +13,6 @@ const BudgetDetailPage = () => { data: subsidyAccessPolicy, } = useSubsidyAccessPolicy(subsidyAccessPolicyId); - console.log('BudgetDetailPage!!!'); - if (isInitialLoadingSubsidyAccessPolicy) { return ( diff --git a/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx b/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx index 732682a35c..1d0ef0db7f 100644 --- a/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx +++ b/src/components/learner-credit-management/BudgetDetailTabsAndRoutes.jsx @@ -43,9 +43,6 @@ const BudgetDetailTabsAndRoutes = ({ enterpriseSlug, enterpriseFeatures, }) => { - - console.log('BudgetDetailTabsAndRoutes!!!'); - const { activeTabKey: routeActiveTabKey } = useParams(); const { budgetId, subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); diff --git a/src/components/learner-credit-management/cards/Collapsibles.jsx b/src/components/learner-credit-management/cards/Collapsibles.jsx index 12d82a6be2..501b113dcf 100644 --- a/src/components/learner-credit-management/cards/Collapsibles.jsx +++ b/src/components/learner-credit-management/cards/Collapsibles.jsx @@ -20,10 +20,6 @@ export const NextStepsForAssignedLearners = ({ course }) => ( is calculated based on the course enrollment deadline or {ASSIGNMENT_ENROLLMENT_DEADLINE} days past the date of assignment, whichever is sooner. -
  • - Learners will receive automated reminder emails every 10-15 days until the enrollment - deadline is reached. -
  • diff --git a/src/components/learner-credit-management/search/CatalogSearchResults.jsx b/src/components/learner-credit-management/search/CatalogSearchResults.jsx index a256e2f5e7..fdd9bde51b 100644 --- a/src/components/learner-credit-management/search/CatalogSearchResults.jsx +++ b/src/components/learner-credit-management/search/CatalogSearchResults.jsx @@ -33,7 +33,6 @@ export const BaseCatalogSearchResults = ({ error, setNoContent, }) => { - console.log('BaseCatalogSearchResults!!!', searchResults); const courseColumns = useMemo( () => [ { diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 462b32d46c..1be7a7488a 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -78,13 +78,17 @@ const mockSuccessfulNotifiedAction = { completedAt: '2023-10-27', errorReason: null, }; - +const mockSuccessfulLinkedLearnerAction = { + uuid: 'test-assignment-action-uuid', + actionType: 'notified', + completedAt: '2023-10-27', + errorReason: null, +}; const mockFailedNotifiedAction = { ...mockSuccessfulNotifiedAction, completedAt: null, - errorReason: 'bad_email', + errorReason: 'email_error', }; - const mockFailedLinkedLearnerAction = { ...mockFailedNotifiedAction, actionType: 'learner_linked', @@ -302,6 +306,7 @@ describe('', () => { learnerState: 'waiting', recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, actions: [mockSuccessfulNotifiedAction], + errorReason: null, }, ], numPages: 1, @@ -373,6 +378,7 @@ describe('', () => { learnerState: 'waiting', recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, actions: [mockSuccessfulNotifiedAction], + errorReason: null, }, ], numPages: 1, @@ -404,6 +410,7 @@ describe('', () => { expectedModalPopupHeading: `Notifying ${mockLearnerEmail}`, expectedModalPopupContent: `Our system is busy emailing ${mockLearnerEmail}!`, actions: [], + errorReason: null, }, { learnerState: 'notifying', @@ -412,6 +419,7 @@ describe('', () => { expectedModalPopupHeading: 'Notifying learner', expectedModalPopupContent: 'Our system is busy emailing the learner!', actions: [], + errorReason: null, }, { learnerState: 'waiting', @@ -419,7 +427,8 @@ describe('', () => { expectedChipStatus: 'Waiting for learner', expectedModalPopupHeading: `Waiting for ${mockLearnerEmail}`, expectedModalPopupContent: 'This learner must create an edX account and complete enrollment in the course', - actions: [mockSuccessfulNotifiedAction], + actions: [mockSuccessfulLinkedLearnerAction, mockSuccessfulNotifiedAction], + errorReason: null, }, { learnerState: 'waiting', @@ -427,7 +436,8 @@ describe('', () => { expectedChipStatus: 'Waiting for learner', expectedModalPopupHeading: 'Waiting for learner', expectedModalPopupContent: 'This learner must create an edX account and complete enrollment in the course', - actions: [mockSuccessfulNotifiedAction], + actions: [mockSuccessfulLinkedLearnerAction, mockSuccessfulNotifiedAction], + errorReason: null, }, { learnerState: 'failed', @@ -435,7 +445,8 @@ describe('', () => { expectedChipStatus: 'Failed: Bad email', expectedModalPopupHeading: 'Failed: Bad email', expectedModalPopupContent: `This course assignment failed because a notification to ${mockLearnerEmail} could not be sent.`, - actions: [mockFailedNotifiedAction], + actions: [mockSuccessfulLinkedLearnerAction, mockFailedNotifiedAction], + errorReason: 'email_error', }, { learnerState: 'failed', @@ -443,7 +454,8 @@ describe('', () => { expectedChipStatus: 'Failed: Bad email', expectedModalPopupHeading: 'Failed: Bad email', expectedModalPopupContent: 'This course assignment failed because a notification to the learner could not be sent.', - actions: [mockFailedNotifiedAction], + actions: [mockSuccessfulLinkedLearnerAction, mockFailedNotifiedAction], + errorReason: 'email_error', }, { learnerState: 'failed', @@ -452,6 +464,7 @@ describe('', () => { expectedModalPopupHeading: 'Failed: System', expectedModalPopupContent: 'Something went wrong behind the scenes.', actions: [mockFailedLinkedLearnerAction], + errorReason: 'internal_api_error', }, ])('renders correct status chips with assigned table data (%s)', ({ learnerState, @@ -460,6 +473,7 @@ describe('', () => { expectedModalPopupHeading, expectedModalPopupContent, actions, + errorReason, }) => { useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, @@ -489,6 +503,7 @@ describe('', () => { learnerState, recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, actions, + errorReason, }, ], numPages: 1, @@ -757,6 +772,7 @@ describe('', () => { learnerState: 'active', recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, actions: [], + errorReason: null, state: 'allocated', }, ], From e97f7ae67b158b748a33535ed04b0eb995858786 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 3 Nov 2023 12:15:56 -0400 Subject: [PATCH 059/124] fix: integrate with `allocate` API for creating assignments, update global retry behavior on queries, handle 404 subsidy access policy (#1080) --- src/components/App/index.jsx | 2 + .../BudgetDetailActivityTabContents.jsx | 5 +- .../BudgetDetailPage.jsx | 15 ++- .../BudgetDetailPageWrapper.jsx | 14 +- .../cards/AssignmentModalContent.jsx | 18 ++- .../cards/BaseCourseCard.jsx | 2 +- .../cards/CourseCard.jsx | 2 +- .../cards/CourseCard.test.jsx | 125 ++++++++++++++---- .../cards/NewAssignmentModalButton.jsx | 68 +++++++++- .../useBudgetDetailActivityOverview.test.jsx | 13 +- .../hooks/useSubsidyAccessPolicy.test.jsx | 13 +- .../tests/BudgetDetailPage.test.jsx | 25 +++- .../tests/CatalogSearch.test.jsx | 7 +- .../tests/CatalogSearchResults.test.jsx | 21 +-- .../tests/NewExistingSSOConfigs.test.jsx | 14 +- .../tests/SettingsSSOTab.test.jsx | 13 +- src/components/test/testUtils.jsx | 27 +++- .../services/EnterpriseAccessApiService.js | 5 + .../tests/EnterpriseAccessApiService.test.js | 13 ++ src/utils.js | 13 ++ src/utils.test.js | 21 +++ 21 files changed, 332 insertions(+), 104 deletions(-) diff --git a/src/components/App/index.jsx b/src/components/App/index.jsx index 5915c030a4..ee1afbbf11 100644 --- a/src/components/App/index.jsx +++ b/src/components/App/index.jsx @@ -23,10 +23,12 @@ import { SystemWideWarningBanner } from '../system-wide-banner'; import store from '../../data/store'; import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { defaultQueryClientRetryHandler } from '../../utils'; const queryClient = new QueryClient({ defaultOptions: { queries: { + retry: defaultQueryClientRetryHandler, // Specifying a longer `staleTime` of 20 seconds means queries will not refetch their data // as often; mitigates making duplicate queries when within the `staleTime` window, instead // relying on the cached data until the `staleTime` window has exceeded. This may be modified diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 123b48f7e5..8c41d33eef 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -14,13 +14,16 @@ const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const { isLoading: isBudgetActivityOverviewLoading, + isFetching: isBudgetActivityOverviewFetching, data: budgetActivityOverview, } = useBudgetDetailActivityOverview({ enterpriseUUID, isTopDownAssignmentEnabled, }); - if (isBudgetActivityOverviewLoading || !budgetActivityOverview) { + // // If the budget activity overview data is loading (either the initial request OR any + // // background re-fetching), show a skeleton. + if (isBudgetActivityOverviewLoading || isBudgetActivityOverviewFetching || !budgetActivityOverview) { return ( <> diff --git a/src/components/learner-credit-management/BudgetDetailPage.jsx b/src/components/learner-credit-management/BudgetDetailPage.jsx index 1ad1ff56cc..3a7192e5e4 100644 --- a/src/components/learner-credit-management/BudgetDetailPage.jsx +++ b/src/components/learner-credit-management/BudgetDetailPage.jsx @@ -5,17 +5,20 @@ import { useBudgetId, useSubsidyAccessPolicy } from './data'; import BudgetDetailTabsAndRoutes from './BudgetDetailTabsAndRoutes'; import BudgetDetailPageWrapper from './BudgetDetailPageWrapper'; import BudgetDetailPageHeader from './BudgetDetailPageHeader'; +import NotFoundPage from '../NotFoundPage'; const BudgetDetailPage = () => { const { subsidyAccessPolicyId } = useBudgetId(); const { - isInitialLoading: isInitialLoadingSubsidyAccessPolicy, data: subsidyAccessPolicy, + isInitialLoading: isSubsidyAccessPolicyInitialLoading, + isError: isSubsidyAccessPolicyError, + error, } = useSubsidyAccessPolicy(subsidyAccessPolicyId); - if (isInitialLoadingSubsidyAccessPolicy) { + if (isSubsidyAccessPolicyInitialLoading) { return ( - + @@ -25,6 +28,12 @@ const BudgetDetailPage = () => { ); } + // If the budget is intended to be a subsidy access policy (by presence of a policy UUID), + // and the subsidy access policy is not found, show 404 messaging. + if (subsidyAccessPolicyId && isSubsidyAccessPolicyError && error?.customAttributes.httpErrorStatus === 404) { + return ; + } + return ( diff --git a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx index 1651094dd4..9d6d95943d 100644 --- a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx @@ -7,7 +7,11 @@ import Hero from '../Hero'; const PAGE_TITLE = 'Learner Credit Management'; -const BudgetDetailPageWrapper = ({ subsidyAccessPolicy, children }) => { +const BudgetDetailPageWrapper = ({ + subsidyAccessPolicy, + includeHero, + children, +}) => { // display name is an optional field, and may not be set for all budgets so fallback to "Overview" // similar to the display name logic for budgets on the overview page route. const budgetDisplayName = subsidyAccessPolicy?.displayName || 'Overview'; @@ -15,7 +19,7 @@ const BudgetDetailPageWrapper = ({ subsidyAccessPolicy, children }) => { return ( <> - + {includeHero && } {children} @@ -26,6 +30,12 @@ const BudgetDetailPageWrapper = ({ subsidyAccessPolicy, children }) => { BudgetDetailPageWrapper.propTypes = { children: PropTypes.node.isRequired, subsidyAccessPolicy: PropTypes.shape(), + includeHero: PropTypes.bool, +}; + +BudgetDetailPageWrapper.defaultProps = { + includeHero: true, + subsidyAccessPolicy: undefined, }; export default BudgetDetailPageWrapper; diff --git a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx index 65bfd3e672..e13e317d58 100644 --- a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx @@ -13,18 +13,25 @@ import BaseCourseCard from './BaseCourseCard'; import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from '../data'; import { ImpactOnYourLearnerCreditBudget, ManagingThisAssignment, NextStepsForAssignedLearners } from './Collapsibles'; -const AssignmentModalContent = ({ course }) => { - const [emailAddresses, setEmailAddresses] = useState(''); +const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { + const [emailAddressesInputValue, setEmailAddressesInputValue] = useState(''); const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const handleEmailAddressInputChange = (e) => { + const inputValue = e.target.value; + const emailAddresses = inputValue.split('\n').filter((email) => email.trim().length > 0); + setEmailAddressesInputValue(inputValue); + onEmailAddressesChange(emailAddresses); + }; + return (

    Use Learner Credit to assign this course

    - +
    @@ -33,8 +40,8 @@ const AssignmentModalContent = ({ course }) => { setEmailAddresses(e.target.value)} + value={emailAddressesInputValue} + onChange={handleEmailAddressInputChange} floatingLabel="Learner email addresses" rows={10} data-hj-suppress @@ -78,6 +85,7 @@ const AssignmentModalContent = ({ course }) => { AssignmentModalContent.propTypes = { course: PropTypes.shape().isRequired, // Pass-thru prop to `BaseCourseCard` + onEmailAddressesChange: PropTypes.func.isRequired, }; export default AssignmentModalContent; diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx index ef81a4f620..a9d0832b8b 100644 --- a/src/components/learner-credit-management/cards/BaseCourseCard.jsx +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -67,7 +67,7 @@ const BaseCourseCard = ({ orientation={isExtraSmall ? 'horizontal' : 'vertical'} textElement={isExecEdCourseType ? execEdEnrollmentInfo : courseEnrollmentInfo} > - {CardFooterActions && } + {CardFooterActions && }
    diff --git a/src/components/learner-credit-management/cards/CourseCard.jsx b/src/components/learner-credit-management/cards/CourseCard.jsx index df2e7379d8..2e1ec52cdb 100644 --- a/src/components/learner-credit-management/cards/CourseCard.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.jsx @@ -8,7 +8,7 @@ import BaseCourseCard from './BaseCourseCard'; const { BUTTON_ACTION } = CARD_TEXT; -const CourseCardFooterActions = (course) => { +const CourseCardFooterActions = ({ course }) => { const { linkToCourse } = course; return [ diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/CourseCard.test.jsx index be225b0c9a..bc6b38aba8 100644 --- a/src/components/learner-credit-management/cards/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.test.jsx @@ -1,17 +1,37 @@ import React from 'react'; -import { screen, within } from '@testing-library/react'; +import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider, useQueryClient } from '@tanstack/react-query'; import { AppContext } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import CourseCard from './CourseCard'; -import { formatPrice, useSubsidyAccessPolicy } from '../data'; +import { + formatPrice, + learnerCreditManagementQueryKeys, + useBudgetId, + useSubsidyAccessPolicy, +} from '../data'; +import { getButtonElement, queryClient } from '../../test/testUtils'; + +import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; + +jest.mock('@tanstack/react-query', () => ({ + ...jest.requireActual('@tanstack/react-query'), + useQueryClient: jest.fn(), +})); + +jest.mock('../data', () => ({ + ...jest.requireActual('../data'), + useBudgetId: jest.fn(), + useSubsidyAccessPolicy: jest.fn(), +})); +jest.mock('../../../data/services/EnterpriseAccessApiService'); const originalData = { availability: ['Upcoming'], @@ -50,7 +70,6 @@ const execEdData = { partners: [{ logo_image_url: '', name: 'Course Provider' }], title: 'Exec Ed Title', }; - const execEdProps = { original: execEdData, }; @@ -66,14 +85,6 @@ const initialStoreState = { }, }; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); - const mockSubsidyAccessPolicy = { uuid: 'test-subsidy-access-policy-uuid', displayName: 'Test Subsidy Access Policy', @@ -81,11 +92,7 @@ const mockSubsidyAccessPolicy = { spendAvailableUsd: 50000, }, }; - -jest.mock('../data', () => ({ - ...jest.requireActual('../data'), - useSubsidyAccessPolicy: jest.fn(), -})); +const mockLearnerEmails = ['hello@example.com', 'world@example.com']; const CourseCardWrapper = ({ initialState = initialStoreState, @@ -94,7 +101,7 @@ const CourseCardWrapper = ({ const store = getMockStore({ ...initialState }); return ( - + { data: mockSubsidyAccessPolicy, isLoading: false, }); + useBudgetId.mockReturnValue({ subsidyAccessPolicyId: mockSubsidyAccessPolicy.uuid }); }); afterEach(() => { @@ -139,7 +147,7 @@ describe('Course card works as expected', () => { const viewCourseCTA = screen.getByText('View course', { selector: 'a' }); expect(viewCourseCTA).toBeInTheDocument(); expect(viewCourseCTA.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/course/course-123x'); - const assignCourseCTA = screen.getByText('Assign', { selector: 'button' }); + const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); }); @@ -167,9 +175,42 @@ describe('Course card works as expected', () => { expect(viewCourseCTA.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/executive-education-2u/course/exec-ed-course-123x'); }); - test('opens assignment modal', () => { + test.each([ + { shouldSubmitAssignments: true, hasAllocationException: true }, + { shouldSubmitAssignments: true, hasAllocationException: false }, + { shouldSubmitAssignments: false, hasAllocationException: false }, + ])('opens assignment modal, submits assignments successfully (%s)', async ({ shouldSubmitAssignments, hasAllocationException }) => { + const mockAllocateContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'allocateContentAssignments'); + if (hasAllocationException) { + mockAllocateContentAssignments.mockRejectedValue(new Error('oops')); + } else { + mockAllocateContentAssignments.mockResolvedValue({ + data: { + updated: [], + created: mockLearnerEmails.map(learnerEmail => ({ + uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f', + assignment_configuration: 'fd456a98-653b-41e9-94d1-94d7b136832a', + learner_email: learnerEmail, + lms_user_id: 0, + content_key: 'string', + content_title: 'string', + content_quantity: 0, + state: 'allocated', + transaction_uuid: '3a6bcbed-b7dc-4791-84fe-b20f12be4001', + last_notification_at: '2019-08-24T14:15:22Z', + actions: [], + })), + no_change: [], + }, + }); + } + useBudgetId.mockReturnValue({ subsidyAccessPolicyId: mockSubsidyAccessPolicy.uuid }); + const mockInvalidateQueries = jest.fn(); + useQueryClient.mockReturnValue({ + invalidateQueries: mockInvalidateQueries, + }); renderWithRouter(); - const assignCourseCTA = screen.getByText('Assign', { selector: 'button' }); + const assignCourseCTA = getButtonElement('Assign'); expect(assignCourseCTA).toBeInTheDocument(); userEvent.click(assignCourseCTA); @@ -190,7 +231,7 @@ describe('Course card works as expected', () => { expect(cardImage).toBeInTheDocument(); expect(cardImage.src).toBeDefined(); expect(modalCourseCard.queryByText('View course', { selector: 'a' })).not.toBeInTheDocument(); - expect(modalCourseCard.queryByText('Assign', { selector: 'button' })).not.toBeInTheDocument(); + expect(getButtonElement('Assign', { screenOverride: modalCourseCard, isQueryByRole: true })).not.toBeInTheDocument(); // Verify empty state and textarea can accept emails expect(assignmentModal.getByText('Assign to')).toBeInTheDocument(); @@ -227,13 +268,43 @@ describe('Course card works as expected', () => { // Verify modal footer expect(assignmentModal.getByText('Help Center: Course Assignments')).toBeInTheDocument(); - const cancelAssignmentCTA = assignmentModal.getByText('Cancel', { selector: 'button' }); + const cancelAssignmentCTA = getButtonElement('Cancel', { screenOverride: assignmentModal }); expect(cancelAssignmentCTA).toBeInTheDocument(); - const submitAssignmentCTA = assignmentModal.getByText('Assign', { selector: 'button' }); + const submitAssignmentCTA = getButtonElement('Assign', { screenOverride: assignmentModal }); expect(submitAssignmentCTA).toBeInTheDocument(); - // Verify modal closes - userEvent.click(cancelAssignmentCTA); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + if (shouldSubmitAssignments) { + // Verify assignment is submitted successfully + userEvent.click(submitAssignmentCTA); + await waitFor(() => expect(mockAllocateContentAssignments).toHaveBeenCalledTimes(1)); + expect(mockAllocateContentAssignments).toHaveBeenCalledWith( + mockSubsidyAccessPolicy.uuid, + expect.objectContaining({ + content_price_cents: 10000, + content_key: 'course-123x', + learner_emails: mockLearnerEmails, + }), + ); + + if (hasAllocationException) { + // Verify error state + expect(getButtonElement('Try again', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'false'); + } else { + // Verify success state + expect(mockInvalidateQueries).toHaveBeenCalledTimes(1); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: learnerCreditManagementQueryKeys.budget(mockSubsidyAccessPolicy.uuid), + }); + expect(getButtonElement('Assigned', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'true'); + // Verify modal closes + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + } + } else { + // Otherwise, verify modal closes when cancel button is clicked + userEvent.click(cancelAssignmentCTA); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + } }); }); diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 1b63968106..e23dbb5273 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -1,16 +1,67 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; +import { useRouteMatch, useHistory, generatePath } from 'react-router-dom'; import { FullscreenModal, ActionRow, Button, useToggle, Hyperlink, + StatefulButton, } from '@edx/paragon'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { snakeCaseObject } from '@edx/frontend-platform/utils'; + import AssignmentModalContent from './AssignmentModalContent'; +import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; +import { learnerCreditManagementQueryKeys, useBudgetId } from '../data'; + +const useAllocateContentAssignments = () => useMutation({ + mutationFn: async ({ + subsidyAccessPolicyId, + payload, + }) => EnterpriseAccessApiService.allocateContentAssignments(subsidyAccessPolicyId, payload), +}); const NewAssignmentModalButton = ({ course, children }) => { + const history = useHistory(); + const routeMatch = useRouteMatch(); + const queryClient = useQueryClient(); + const { subsidyAccessPolicyId } = useBudgetId(); + const [isOpen, open, close] = useToggle(false); + const [learnerEmails, setLearnerEmails] = useState([]); + const [assignButtonState, setAssignButtonState] = useState('default'); + + const { mutate } = useAllocateContentAssignments(); + + const pathToActivityTab = generatePath(routeMatch.path, { budgetId: subsidyAccessPolicyId, activeTabKey: 'activity' }); + + const handleAllocateContentAssignments = () => { + const payload = snakeCaseObject({ + contentPriceCents: course.normalizedMetadata.contentPrice * 100, // Convert to USD cents + contentKey: course.key, + learnerEmails, + }); + const mutationArgs = { + subsidyAccessPolicyId, + payload, + }; + setAssignButtonState('pending'); + mutate(mutationArgs, { + onSuccess: () => { + setAssignButtonState('complete'); + queryClient.invalidateQueries({ + queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), + }); + close(); + history.push(pathToActivityTab); + }, + onError: () => { + setAssignButtonState('error'); + }, + }); + }; return ( <> @@ -27,12 +78,21 @@ const NewAssignmentModalButton = ({ course, children }) => { - {/* TODO: https://2u-internal.atlassian.net/browse/ENT-7826 */} - + )} > - + ); diff --git a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx index cdb3b3e9bf..3290ef90dc 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx @@ -1,4 +1,4 @@ -import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; import useBudgetDetailActivityOverview from './useBudgetDetailActivityOverview'; @@ -12,22 +12,15 @@ import { mockEnterpriseOfferId, mockSubsidyAccessPolicyUUID, } from '../tests/constants'; +import { queryClient } from '../../../test/testUtils'; jest.mock('./useBudgetId'); jest.mock('./useSubsidyAccessPolicy'); const mockEnterpriseUUID = 'mock-enterprise-uuid'; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); - const wrapper = ({ children }) => ( - + {children} ); diff --git a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx index 30edc0f3cc..cc773fcb79 100644 --- a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx @@ -1,8 +1,9 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; // Import the hook import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import { queryClient } from '../../../test/testUtils'; const mockSubsidyAccessPolicyUUID = '9af340a9-48de-4d94-976d-e2282b9eb7f3'; const mockAssignmentConfiguration = { uuid: 'test-assignment-configuration-uuid' }; @@ -18,16 +19,8 @@ jest.mock('../../../../data/services/EnterpriseAccessApiService', () => ({ }), })); -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); - const wrapper = ({ children }) => ( - {children} + {children} ); describe('useSubsidyAccessPolicy', () => { diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 1be7a7488a..6c0a0c8040 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { useParams } from 'react-router-dom'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; @@ -29,6 +29,7 @@ import { mockSubsidyAccessPolicyUUID, mockEnterpriseOfferId, } from '../data/tests/constants'; +import { queryClient } from '../../test/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -98,8 +99,6 @@ const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; -const queryClient = new QueryClient(); - const BudgetDetailPageWrapper = ({ initialState = initialStoreState, enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue, @@ -107,7 +106,7 @@ const BudgetDetailPageWrapper = ({ }) => { const store = getMockStore({ ...initialState }); return ( - + @@ -124,6 +123,24 @@ describe('', () => { jest.resetAllMocks(); }); + it('renders page not found messaging if budget is a subsidy access policy, but the REST API returns a 404', () => { + useParams.mockReturnValue({ + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + isError: true, + error: { customAttributes: { httpErrorStatus: 404 } }, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: mockEmptyStateBudgetDetailActivityOverview, + }); + renderWithRouter(); + expect(screen.getByText('404', { selector: 'h1' })); + }); + it.each([ { displayName: null }, { displayName: 'Test Budget Display Name' }, diff --git a/src/components/learner-credit-management/tests/CatalogSearch.test.jsx b/src/components/learner-credit-management/tests/CatalogSearch.test.jsx index ab6a0e6a01..dc17350f21 100644 --- a/src/components/learner-credit-management/tests/CatalogSearch.test.jsx +++ b/src/components/learner-credit-management/tests/CatalogSearch.test.jsx @@ -6,8 +6,8 @@ import { } from '@edx/frontend-enterprise-catalog-search'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { screen } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderWithRouter } from '../../test/testUtils'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { queryClient, renderWithRouter } from '../../test/testUtils'; import CatalogSearch from '../search/CatalogSearch'; import { useBudgetId, useSubsidyAccessPolicy } from '../data'; @@ -21,10 +21,9 @@ jest.mock('react-instantsearch-dom', () => ({ jest.mock('../data'); const DEFAULT_SEARCH_CONTEXT_VALUE = { refinements: {} }; -const queryClient = new QueryClient(); const SearchDataWrapper = ({ children, searchContextValue }) => ( - + { const store = getMockStore({ ...initialStoreState }); return ( - - - {children} - - + + + + {children} + + + ); }; @@ -153,7 +156,7 @@ describe('Main Catalogs view works as expected', () => { }); test('all courses rendered when search results available', async () => { - render( + renderWithRouter( diff --git a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx index cd0b7dfdd1..5ac24d2601 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx @@ -1,9 +1,6 @@ import React from 'react'; import '@testing-library/jest-dom/extend-expect'; -import { - QueryClient, - QueryClientProvider, -} from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import userEvent from '@testing-library/user-event'; import { act, @@ -19,12 +16,7 @@ import { features } from '../../../../config'; import NewExistingSSOConfigs from '../NewExistingSSOConfigs'; import { SSOConfigContext, SSO_INITIAL_STATE } from '../SSOConfigContext'; import LmsApiService from '../../../../data/services/LmsApiService'; - -const queryClient = new QueryClient({ - queries: { - retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis. - }, -}); +import { queryClient } from '../../../test/testUtils'; jest.mock('../../utils'); jest.mock('../../../../data/services/LmsApiService'); @@ -152,7 +144,7 @@ const setupNewExistingSSOConfigs = (configs) => { features.AUTH0_SELF_SERVICE_INTEGRATION = true; return render( - + mockStore(aStore); @@ -102,7 +95,7 @@ describe('SAML Config Tab', () => { })); await waitFor(() => render( - + , diff --git a/src/components/test/testUtils.jsx b/src/components/test/testUtils.jsx index e4a22ba445..931afffc6e 100644 --- a/src/components/test/testUtils.jsx +++ b/src/components/test/testUtils.jsx @@ -2,8 +2,10 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; -import { render, screen } from '@testing-library/react'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { QueryClient } from '@tanstack/react-query'; +// TODO: this could likely be replaced by `renderWithRouter` from `@edx/frontend-enterprise-utils`. export function renderWithRouter( ui, { @@ -30,4 +32,25 @@ export function findElementWithText(container, type, text) { return [...elements].find((elem) => elem.innerHTML.includes(text)); } -export const getButtonElement = (buttonText) => screen.getByRole('button', { name: buttonText }); +export const getButtonElement = (buttonText, options = {}) => { + const { + screenOverride, + isQueryByRole, + } = options; + const screen = screenOverride || rtlScreen; + if (isQueryByRole) { + return screen.queryByRole('button', { name: buttonText }); + } + return screen.getByRole('button', { name: buttonText }); +}; + +export function queryClient(options = {}) { + return new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + ...options, + }, + }); +} diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index be704fe658..ad431633c3 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -166,6 +166,11 @@ class EnterpriseAccessApiService { const url = `${EnterpriseAccessApiService.baseUrl}/subsidy-access-policies/${subsidyAccessPolicyUUID}/`; return EnterpriseAccessApiService.apiClient().get(url); } + + static allocateContentAssignments(subsidyAccessPolicyUUID, payload) { + const url = `${EnterpriseAccessApiService.baseUrl}/policy-allocation/${subsidyAccessPolicyUUID}/allocate/`; + return EnterpriseAccessApiService.apiClient().post(url, payload); + } } export default EnterpriseAccessApiService; diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index fd1497705d..7fd27b22a1 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -152,4 +152,17 @@ describe('EnterpriseAccessApiService', () => { `${enterpriseAccessBaseUrl}/api/v1/subsidy-access-policies/${mockSubsidyAccessPolicyUUID}/`, ); }); + + test('allocateContentAssignments calls enterprise-access allocate POST API to create assignments', () => { + const payload = { + learner_emails: ['edx@example.com'], + content_key: 'edX+DemoX', + content_price_cents: 19900, + }; + EnterpriseAccessApiService.allocateContentAssignments(mockSubsidyAccessPolicyUUID, payload); + expect(axios.post).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/policy-allocation/${mockSubsidyAccessPolicyUUID}/allocate/`, + payload, + ); + }); }); diff --git a/src/utils.js b/src/utils.js index 871af1524a..207d25c8bc 100644 --- a/src/utils.js +++ b/src/utils.js @@ -400,6 +400,18 @@ const pollAsync = async (pollFunc, timeout, interval, checkFunc) => { return false; }; +/** + * Modifies the retry behavior of queries to retry up to max 3 times (default) or if + * the error returned by the query is a 404 HTTP status code (not found). This configuration + * may be overridden per-query, as needed. + */ +function defaultQueryClientRetryHandler(failureCount, err) { + if (failureCount >= 3 || err.customAttributes.httpErrorStatus === 404) { + return false; + } + return true; +} + export { camelCaseDict, camelCaseDictArray, @@ -433,4 +445,5 @@ export { capitalizeFirstLetter, pollAsync, isNotValidNumberString, + defaultQueryClientRetryHandler, }; diff --git a/src/utils.test.js b/src/utils.test.js index c2c713bdc1..977582e89c 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -5,6 +5,7 @@ import { snakeCaseFormData, pollAsync, isValidNumber, + defaultQueryClientRetryHandler, } from './utils'; describe('utils', () => { @@ -93,4 +94,24 @@ describe('utils', () => { expect(isValidNumber(undefined)).toEqual(false); }); }); + + describe('defaultQueryClientRetryHandler', () => { + const mockError404 = { customAttributes: { httpErrorStatus: 404 } }; + const mockError500 = { customAttributes: { httpErrorStatus: 500 } }; + + it.each([3, 4])('return false if failureCount >= 3 (failureCount: %s)', (failureCount) => { + const result = defaultQueryClientRetryHandler(failureCount, mockError500); + expect(result).toEqual(false); + }); + + it('return false if error is a 404 HTTP status code', () => { + const result = defaultQueryClientRetryHandler(1, mockError404); + expect(result).toEqual(false); + }); + + it.each([1, 2])('return true if first failure and error is not a 404 (failureCount: %s)', (failureCount) => { + const result = defaultQueryClientRetryHandler(failureCount, mockError500); + expect(result).toEqual(true); + }); + }); }); From bb42b6020019280053843440d7df47aeeae8bf50 Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:57:57 -0700 Subject: [PATCH 060/124] fix: altering bulk enrollment action behaviour (#1081) * fix: altering bulk enrollment action behaviour * fix: PR requests * fix: PR requests --- .../tests/DeleteHighlightSet.test.jsx | 2 +- .../AssignmentRowActionTableCell.jsx | 56 ++++++------ .../AssignmentTableRemind.jsx | 17 ++-- .../tests/BudgetDetailPage.test.jsx | 88 +++++-------------- .../steps/SSOConfigConfigureStep.jsx | 2 +- .../services/EnterpriseAccessApiService.js | 2 +- 6 files changed, 59 insertions(+), 108 deletions(-) diff --git a/src/components/ContentHighlights/tests/DeleteHighlightSet.test.jsx b/src/components/ContentHighlights/tests/DeleteHighlightSet.test.jsx index 336cb008c0..ffb283c5fe 100644 --- a/src/components/ContentHighlights/tests/DeleteHighlightSet.test.jsx +++ b/src/components/ContentHighlights/tests/DeleteHighlightSet.test.jsx @@ -97,7 +97,7 @@ describe('', () => { expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); }); - it('cancelling confirmation modal closes modal', () => { + it('canceling confirmation modal closes modal', () => { renderWithRouter( , { route: initialRouterEntry }, diff --git a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx index 1d771c27fa..a180598d31 100644 --- a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx @@ -1,62 +1,56 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - Icon, - IconButton, - OverlayTrigger, - Stack, - Tooltip, + Icon, IconButton, OverlayTrigger, Stack, Tooltip, } from '@edx/paragon'; import { Mail, DoNotDisturbOn } from '@edx/paragon/icons'; const AssignmentRowActionTableCell = ({ row }) => { - const cancelButtonMarginLeft = row.original.state === 'allocated' ? 'ml-2.5' : 'ml-auto'; + const isLearnerStateWaiting = row.original.learnerState === 'waiting'; + const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; return ( -
    - {row.original.state === 'allocated' && ( - <> - Remind learner} - > - console.log(`Reminding ${row.original.uuid}`)} - data-testid={`remind-assignment-${row.original.uuid}`} - /> - - - + + {isLearnerStateWaiting && ( + Remind learner} + > + console.log(`Reminding ${row.original.uuid}`)} + data-testid={`remind-assignment-${row.original.uuid}`} + /> + )} Cancel assignment} + overlay={Cancel assignment} > console.log(`Canceling ${row.original.uuid}`)} data-testid={`cancel-assignment-${row.original.uuid}`} /> -
    + ); }; AssignmentRowActionTableCell.propTypes = { row: PropTypes.shape({ original: PropTypes.shape({ + learnerEmail: PropTypes.string, + learnerState: PropTypes.string, uuid: PropTypes.string.isRequired, - state: PropTypes.string.isRequired, }).isRequired, }).isRequired, }; diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx index bee8cc9814..cc62c9a703 100644 --- a/src/components/learner-credit-management/AssignmentTableRemind.jsx +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -4,16 +4,15 @@ import { Button } from '@edx/paragon'; import { Mail } from '@edx/paragon/icons'; const AssignmentTableRemindAction = ({ selectedFlatRows, ...rest }) => { - const hideRemindAction = selectedFlatRows.some( - row => row.original.state !== 'allocated', - ); - if (hideRemindAction) { - return null; - } + const selectedRemindableRows = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').length; return ( - // eslint-disable-next-line no-console - ); }; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 6c0a0c8040..5f3beec3a5 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -756,7 +756,16 @@ describe('', () => { expect(screen.getByText('loading budget activity overview')).toBeInTheDocument(); }); - it('displays remind row and bulk actions when allocated', async () => { + it.each([ + { + learnerState: 'waiting', + shouldDisplayRemindAction: true, + }, + { + learnerState: 'notifying', + shouldDisplayRemindAction: false, + }, + ])('displays remind and cancel row and bulk actions when appropriate (%s)', async ({ learnerState, shouldDisplayRemindAction }) => { useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', @@ -786,7 +795,7 @@ describe('', () => { uuid: 'test-uuid', contentKey: mockCourseKey, contentQuantity: -19900, - learnerState: 'active', + learnerState, recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, actions: [], errorReason: null, @@ -800,73 +809,22 @@ describe('', () => { }); renderWithRouter(); const cancelRowAction = screen.getByTestId('cancel-assignment-test-uuid'); - const remindRowAction = screen.getByTestId('remind-assignment-test-uuid'); expect(cancelRowAction).toBeInTheDocument(); - expect(remindRowAction).toBeInTheDocument(); + if (shouldDisplayRemindAction) { + const remindRowAction = screen.getByTestId('remind-assignment-test-uuid'); + expect(remindRowAction).toBeInTheDocument(); + } const checkBox = screen.getByTestId('datatable-select-column-checkbox-cell'); expect(checkBox).toBeInTheDocument(); userEvent.click(checkBox); - await waitFor(() => { - expect(screen.getByText('Remind (1)')).toBeInTheDocument(); - }); - await waitFor(() => { - expect(screen.getByText('Cancel (1)')).toBeInTheDocument(); - }); - }); - - it('hides remind row and bulk actions when allocated', () => { - useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), - }); - useBudgetDetailActivityOverview.mockReturnValue({ - isLoading: false, - data: { - contentAssignments: { count: 1 }, - spentTransactions: { count: 0 }, - }, - }); - useParams.mockReturnValue({ - budgetId: mockSubsidyAccessPolicyUUID, - activeTabKey: 'activity', - }); - useSubsidyAccessPolicy.mockReturnValue({ - isInitialLoading: false, - data: mockAssignableSubsidyAccessPolicy, - }); - useBudgetDetailActivityOverview.mockReturnValue({ - isLoading: false, - data: { - contentAssignments: { count: 1 }, - spentTransactions: { count: 0 }, - }, - }); - useBudgetContentAssignments.mockReturnValue({ - isLoading: false, - contentAssignments: { - count: 1, - results: [ - { - uuid: 'test-uuid', - contentKey: mockCourseKey, - contentQuantity: -19900, - learnerState: 'accepted', - recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, - actions: [], - state: 'accepted', - }, - ], - numPages: 1, - currentPage: 1, - }, - fetchContentAssignments: jest.fn(), - }); - renderWithRouter(); - expect(screen.queryByTestId('remind-assignment-test-uuid')).not.toBeInTheDocument(); - const checkBox = screen.getByTestId('datatable-select-column-checkbox-cell'); - userEvent.click(checkBox); - expect(screen.queryByText('Remind (1)')).not.toBeInTheDocument(); + expect(await screen.findByText('Cancel (1)')).toBeInTheDocument(); + if (shouldDisplayRemindAction) { + expect(await screen.findByText('Remind (1)')).toBeInTheDocument(); + } else { + const remindButton = await screen.findByText('Remind (0)'); + expect(remindButton).toBeInTheDocument(); + expect(remindButton).toBeDisabled(); + } }); }); diff --git a/src/components/settings/SettingsSSOTab/steps/SSOConfigConfigureStep.jsx b/src/components/settings/SettingsSSOTab/steps/SSOConfigConfigureStep.jsx index d5c61adc1c..62c615e8fb 100644 --- a/src/components/settings/SettingsSSOTab/steps/SSOConfigConfigureStep.jsx +++ b/src/components/settings/SettingsSSOTab/steps/SSOConfigConfigureStep.jsx @@ -449,7 +449,7 @@ const SSOConfigConfigureStep = ({ /> Configurable value that represents the amount of time in seconds, no greater than 30, that the - edX system will wait for a response before cancelling the request. + edX system will wait for a response before canceling the request. {!odataApiTimeoutIntervalValid && ( diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index ad431633c3..b115f194fb 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -151,7 +151,7 @@ class EnterpriseAccessApiService { page: 1, page_size: 25, // Only include assignments with allocated or errored states. The table should NOT - // include assignments in the cancelled or accepted states. + // include assignments in the canceled or accepted states. state__in: 'allocated,errored', ...snakeCaseObject(options), }); From 1f1c1ac29292cd31ab77cae49f5197e333231518 Mon Sep 17 00:00:00 2001 From: Marlon Keating <322346+marlonkeating@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:26:57 -0800 Subject: [PATCH 061/124] feat: Hook up sso config workflow to api (#1072) chore: Address code review feedback chore: Address code review feedback fix: SAP field validation fix: Add username field --- src/components/forms/FormContextWrapper.tsx | 16 +- src/components/forms/FormWorkflow.tsx | 32 +++- src/components/forms/ValidatedFormControl.tsx | 10 +- src/components/forms/ValidatedFormRadio.tsx | 13 +- src/components/forms/data/reducer.test.ts | 2 + src/components/forms/data/reducer.ts | 7 +- .../SettingsLMSTab/UnsavedChangesModal.tsx | 8 +- .../settings/SettingsSSOTab/NewSSOStepper.jsx | 35 +++- .../SettingsSSOTab/SSOFormWorkflowConfig.tsx | 134 +++++++++++++- .../SettingsSSOTab/UnsavedSSOChangesModal.tsx | 36 ++++ .../steps/NewSSOConfigAuthorizeStep.tsx | 120 ++++++++---- .../steps/NewSSOConfigConfigureStep.tsx | 78 ++++++-- .../steps/NewSSOConfigConnectStep.tsx | 98 +++++++--- .../tests/NewSSOConfigForm.test.jsx | 173 +++++++++++++----- 14 files changed, 584 insertions(+), 178 deletions(-) create mode 100644 src/components/settings/SettingsSSOTab/UnsavedSSOChangesModal.tsx diff --git a/src/components/forms/FormContextWrapper.tsx b/src/components/forms/FormContextWrapper.tsx index a6d745f896..fa3c8a2b75 100644 --- a/src/components/forms/FormContextWrapper.tsx +++ b/src/components/forms/FormContextWrapper.tsx @@ -1,11 +1,15 @@ import React, { useReducer } from 'react'; import FormContextProvider from './FormContext'; -import FormWorkflow, { FormWorkflowProps } from './FormWorkflow'; +import FormWorkflow, { DynamicComponent, FormWorkflowProps, UnsavedChangesModalProps } from './FormWorkflow'; import { FormReducer, FormReducerType, initializeForm, InitializeFormArguments, } from './data/reducer'; +import DefaultUnsavedChangesModal from '../settings/SettingsLMSTab/UnsavedChangesModal'; -type FormWrapperProps = FormWorkflowProps & { formData: FormConfigData }; +type FormWrapperProps = FormWorkflowProps & { + formData: FormConfigData; + unsavedChangesModal?: DynamicComponent; +}; const FormContextWrapper = ({ workflowTitle, @@ -13,6 +17,7 @@ const FormContextWrapper = ({ onClickOut, formData, isStepperOpen, + UnsavedChangesModal, }: FormWrapperProps) => { const initializeAction: InitializeFormArguments = { formFields: formData as FormConfigData, @@ -33,7 +38,12 @@ const FormContextWrapper = ({ > diff --git a/src/components/forms/FormWorkflow.tsx b/src/components/forms/FormWorkflow.tsx index d3bb072534..80288c075a 100644 --- a/src/components/forms/FormWorkflow.tsx +++ b/src/components/forms/FormWorkflow.tsx @@ -8,10 +8,14 @@ import { Launch } from '@edx/paragon/icons'; import { useFormContext } from './FormContext'; import type { FormFieldValidation, FormContext } from './FormContext'; import { - FORM_ERROR_MESSAGE, FormActionArguments, setStepAction, setWorkflowStateAction, setShowErrorsAction, + FORM_ERROR_MESSAGE, + FormActionArguments, + setStepAction, + setWorkflowStateAction, + setShowErrorsAction, + updateFormFieldsAction, } from './data/actions'; import { HELP_CENTER_LINK, SUBMIT_TOAST_MESSAGE } from '../settings/data/constants'; -import UnsavedChangesModal from '../settings/SettingsLMSTab/UnsavedChangesModal'; import ConfigErrorModal from '../settings/ConfigErrorModal'; import { channelMapping, pollAsync } from '../../utils'; import HelpCenterButton from '../settings/HelpCenterButton'; @@ -21,7 +25,7 @@ export const WAITING_FOR_ASYNC_OPERATION = 'WAITING FOR ASYNC OPERATION'; export type FormWorkflowErrorHandler = (errMsg: string) => void; export type FormWorkflowHandlerArgs = { - formFields?: FormData; + formFields: FormData; formFieldsChanged: boolean; errHandler?: FormWorkflowErrorHandler; dispatch?: Dispatch; @@ -41,12 +45,15 @@ export type FormWorkflowButtonConfig = { awaitSuccess?: FormWorkflowAwaitHandler; }; -type DynamicComponent = React.FunctionComponent | React.ComponentClass | React.ElementType; +export type DynamicComponent = + | React.FunctionComponent + | React.ComponentClass + | React.ElementType; export type FormWorkflowStep = { index: number; stepName: string; - formComponent: DynamicComponent; + formComponent: DynamicComponent; validations: FormFieldValidation[]; saveChanges?: ( formData: FormData, @@ -62,12 +69,20 @@ export type FormWorkflowConfig = { getCurrentStep: () => FormWorkflowStep; }; +export type UnsavedChangesModalProps = { + isOpen: boolean; + close: () => void; + exitWithoutSaving?: () => void; + saveDraft?: () => void +}; + export type FormWorkflowProps = { workflowTitle: string; formWorkflowConfig: FormWorkflowConfig; onClickOut: (() => void) | ((edited?: boolean, msg?: string) => null); dispatch: Dispatch; isStepperOpen: boolean; + UnsavedChangesModal?: DynamicComponent; }; // Modal container for multi-step forms @@ -77,6 +92,7 @@ const FormWorkflow = ({ onClickOut, isStepperOpen, dispatch, + UnsavedChangesModal, }: FormWorkflowProps) => { const { formFields, @@ -121,6 +137,9 @@ const FormWorkflow = ({ dispatch, formFieldsChanged: !!isEdited, }); + if (newFormFields) { + dispatch(updateFormFieldsAction({ formFields: newFormFields })); + } if (nextButtonConfig?.awaitSuccess) { advance = await pollAsync( () => nextButtonConfig.awaitSuccess?.awaitCondition?.({ @@ -165,7 +184,7 @@ const FormWorkflow = ({ const stepBody = (currentStep: FormWorkflowStep) => { if (currentStep) { - const FormComponent: DynamicComponent = currentStep?.formComponent; + const FormComponent: DynamicComponent = currentStep?.formComponent; return ( ({ close={clearFormError} configTextOverride={stateMap && stateMap[FORM_ERROR_MESSAGE]} /> + {/* @ts-ignore JSX element type 'UnsavedChangesModal' does not have any construct or call signatures. */} { id: props.formId, value: formFields && formFields[props.formId], }; - // we need to set the original values on load in order to trigger the validation - useEffect(() => { - if (dispatch) { - dispatch( - setFormFieldAction({ fieldId: props.formId, value: formControlProps.value }), - ); - } - }, [dispatch, props.formId, formControlProps.value]); return ( <> diff --git a/src/components/forms/ValidatedFormRadio.tsx b/src/components/forms/ValidatedFormRadio.tsx index a37e6e0b8c..bfc29fb2d2 100644 --- a/src/components/forms/ValidatedFormRadio.tsx +++ b/src/components/forms/ValidatedFormRadio.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect } from 'react'; +import React, { ReactElement } from 'react'; import omit from 'lodash/omit'; import isString from 'lodash/isString'; @@ -30,7 +30,7 @@ const ValidatedFormRadio = (props: ValidatedFormRadioProps) => { dispatch(setFormFieldAction({ fieldId: props.formId, value: e.target.value })); } }; - + const value = formFields?.[props?.formId]; const errors = errorMap?.[props.formId]; // Show error message if an error message was part of any detected errors const showError = errors?.find?.(error => isString(error)); @@ -41,14 +41,6 @@ const ValidatedFormRadio = (props: ValidatedFormRadioProps) => { id: props.formId, value: formFields && formFields[props.formId], }; - // we need to set the original values on load in order to trigger the validation - useEffect(() => { - if (dispatch) { - dispatch( - setFormFieldAction({ fieldId: props.formId, value: formRadioProps.value }), - ); - } - }, [dispatch, props.formId, formRadioProps.value]); const createOptions = (options: [string, string][]) => { const optionList: ReactElement[] = []; @@ -71,6 +63,7 @@ const ValidatedFormRadio = (props: ValidatedFormRadioProps) => { name={formRadioProps.id} onChange={formRadioProps.onChange} isInline={formRadioProps.isInline} + value={value} > {createOptions(formRadioProps.options)} diff --git a/src/components/forms/data/reducer.test.ts b/src/components/forms/data/reducer.test.ts index 6c3838d6fa..988102d634 100644 --- a/src/components/forms/data/reducer.test.ts +++ b/src/components/forms/data/reducer.test.ts @@ -80,6 +80,8 @@ describe('Form reducer tests', () => { }; expect(initializeForm(initializeFormArguments)).toEqual({ formFields, + errorMap: {}, + hasErrors: false, currentStep: steps[0], isEdited: false, }); diff --git a/src/components/forms/data/reducer.ts b/src/components/forms/data/reducer.ts index 1aa424be21..afee5e8c8c 100644 --- a/src/components/forms/data/reducer.ts +++ b/src/components/forms/data/reducer.ts @@ -86,9 +86,7 @@ export function initializeForm(action: InitializeFormArguments FormContext; @@ -120,7 +118,8 @@ export const FormReducer: FormReducerType = ( }; } case SET_STEP: { const setStepArgs = action as SetStepArguments; - return { ...state, currentStep: setStepArgs.step }; + const newStepState = { ...state, currentStep: setStepArgs.step }; + return processFormErrors(newStepState); } case SET_SHOW_ERRORS: { const SetShowErrorsArgs = action as SetShowErrorsArguments; return { ...state, showErrors: SetShowErrorsArgs.showErrors }; diff --git a/src/components/settings/SettingsLMSTab/UnsavedChangesModal.tsx b/src/components/settings/SettingsLMSTab/UnsavedChangesModal.tsx index ac09b409aa..1eba8b9e8d 100644 --- a/src/components/settings/SettingsLMSTab/UnsavedChangesModal.tsx +++ b/src/components/settings/SettingsLMSTab/UnsavedChangesModal.tsx @@ -1,16 +1,10 @@ import React from 'react'; import { ModalDialog, ActionRow, Button } from '@edx/paragon'; +import { UnsavedChangesModalProps } from '../../forms/FormWorkflow'; const MODAL_TITLE = 'Exit configuration'; const MODAL_TEXT = 'Your configuration data will be saved under your Learning Platform settings'; -type UnsavedChangesModalProps = { - isOpen: boolean; - close: () => void; - exitWithoutSaving: () => void; - saveDraft: () => void -}; - // will have to pass in individual saveDraft method and config when // drafting is allowed const UnsavedChangesModal = ({ diff --git a/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx b/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx index 55ae8eb1f0..80f23b4d69 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx @@ -1,31 +1,54 @@ -import React, { useState, useContext } from 'react'; +import React, { + useState, useContext, +} from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import FormContextWrapper from '../../forms/FormContextWrapper'; import { SSOConfigContext } from './SSOConfigContext'; import SSOFormWorkflowConfig from './SSOFormWorkflowConfig'; +import { camelCaseDict } from '../../../utils'; +import UnsavedSSOChangesModal from './UnsavedSSOChangesModal'; +import { IDP_URL_SELECTION, IDP_XML_SELECTION } from './steps/NewSSOConfigConnectStep'; -const NewSSOStepper = () => { +const NewSSOStepper = ({ enterpriseId }) => { const { - setProviderConfig, + setProviderConfig, refreshBool, setRefreshBool, ssoState: { providerConfig }, } = useContext(SSOConfigContext); + const providerConfigCamelCase = camelCaseDict(providerConfig || {}); const [isStepperOpen, setIsStepperOpen] = useState(true); const handleCloseWorkflow = () => { setProviderConfig?.(null); setIsStepperOpen(false); + setRefreshBool(!refreshBool); }; + if (providerConfigCamelCase.metadataXml || providerConfigCamelCase.metadataUrl) { + providerConfigCamelCase.idpConnectOption = providerConfigCamelCase?.metadataUrl + ? IDP_URL_SELECTION + : IDP_XML_SELECTION; + } return (isStepperOpen && (
    ) ); }; -export default NewSSOStepper; +NewSSOStepper.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; + +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(NewSSOStepper); diff --git a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx index dcaff6c46e..b27a8df687 100644 --- a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx +++ b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx @@ -1,37 +1,151 @@ -import type { FormWorkflowStep } from '../../forms/FormWorkflow'; -import SSOConfigConnectStep from './steps/NewSSOConfigConnectStep'; -import SSOConfigConfigureStep from './steps/NewSSOConfigConfigureStep'; -import SSOConfigAuthorizeStep from './steps/NewSSOConfigAuthorizeStep'; +import omit from 'lodash/omit'; + +import type { FormWorkflowHandlerArgs, FormWorkflowStep } from '../../forms/FormWorkflow'; +import SSOConfigConnectStep, { validations as SSOConfigConnectStepValidations } from './steps/NewSSOConfigConnectStep'; +import SSOConfigConfigureStep, { validations as SSOConfigConfigureStepValidations } from './steps/NewSSOConfigConfigureStep'; +import SSOConfigAuthorizeStep, { validations as SSOConfigAuthorizeStepValidations } from './steps/NewSSOConfigAuthorizeStep'; import SSOConfigConfirmStep from './steps/NewSSOConfigConfirmStep'; +import LmsApiService from '../../../data/services/LmsApiService'; +import handleErrors from '../utils'; +import { snakeCaseDict } from '../../../utils'; + +type SSOConfigSnakeCase = { + uuid?: string, + enterprise_customer: string, + is_removed: boolean, + active: boolean, + identity_provider: string, + metadata_url: string, + metadata_xml: string, + entity_id: string, + update_from_metadata: boolean, + user_id_attribute: string, + full_name_attribute: string, + last_name_attribute: string, + email_attribute: string, + username_attribute: string, + country_attribute: string, + submitted_at: null, + configured_at: null, + validated_at: null, + odata_api_timeout_interval: null, + odata_api_root_url: string, + odata_company_id: string, + sapsf_oauth_root_url: string, + odata_api_request_timeout: null, + sapsf_private_key: string, + odata_client_id: string, + oauth_user_id: string, + sp_metadata_url?: string +}; + +type SSOConfigCamelCase = { + uuid?: string, + enterpriseCustomer: string, + isRemoved: boolean, + active: boolean, + identityProvider: string, + metadataUrl: string, + metadataXml: string, + entityId: string, + updateFromMetadata: boolean, + userIdAttribute: string, + fullNameAttribute: string, + firstNameAttribute: string, + lastNameAttribute: string, + emailAttribute: string, + usernameAttribute: string, + countryAttribute: string, + submittedAt: null, + configuredAt: null, + validatedAt: null, + odataApiTimeoutInterval: null, + odataApiRootUrl: string, + odataCompanyId: string, + sapsfOauthRootUrl: string, + odataApiRequestTimeout: null, + sapsfPrivateKey: string, + odataClientId: string, + oauthUserId: string, + spMetadataUrl?: string +}; + +type SSOConfigFormControlVariables = { + idpConnectOption?: boolean, + confirmAuthorizedEdxServiceProvider?: boolean +}; -type SSOConfigCamelCase = {}; +type SSOConfigFormContextData = SSOConfigCamelCase & SSOConfigFormControlVariables; -export const SSOFormWorkflowConfig = () => { +export const SSOFormWorkflowConfig = ({ enterpriseId }) => { const placeHolderButton = (buttonName?: string) => () => ({ buttonText: buttonName || 'Next', opensNewWindow: false, onClick: () => {}, }); + const saveChanges = async ({ + formFields, + errHandler, + formFieldsChanged, + }:FormWorkflowHandlerArgs) => { + let err = null; + if (!formFieldsChanged) { + // Don't submit if nothing has changed + return formFields; + } + let updatedFormFields: SSOConfigCamelCase = omit(formFields, ['idpConnectOption', 'spMetadataUrl', 'isPendingConfiguration']); + updatedFormFields.enterpriseCustomer = enterpriseId; + const submittedFormFields: SSOConfigSnakeCase = snakeCaseDict(updatedFormFields) as SSOConfigSnakeCase; + if (submittedFormFields?.uuid) { + try { + const updateResponse = await LmsApiService.updateEnterpriseSsoOrchestrationRecord( + submittedFormFields, + formFields?.uuid, + ); + updatedFormFields = updateResponse.data; + } catch (error) { + err = handleErrors(error); + } + } else { + try { + const createResponse = await LmsApiService.createEnterpriseSsoOrchestrationRecord(submittedFormFields); + updatedFormFields.uuid = createResponse.data.record; + updatedFormFields.spMetadataUrl = createResponse.data.sp_metadata_url; + } catch (error) { + err = handleErrors(error); + } + } + if (err && errHandler) { + errHandler(err); + } + const newFormFields = { ...formFields, ...updatedFormFields } as SSOConfigCamelCase; + return newFormFields; + }; + const steps: FormWorkflowStep[] = [ { index: 0, formComponent: SSOConfigConnectStep, - validations: [], + validations: SSOConfigConnectStepValidations, stepName: 'Connect', nextButtonConfig: placeHolderButton(), }, { index: 1, formComponent: SSOConfigConfigureStep, - validations: [], + validations: SSOConfigConfigureStepValidations, stepName: 'Configure', - nextButtonConfig: placeHolderButton('Configure'), + nextButtonConfig: () => ({ + buttonText: 'Configure', + opensNewWindow: false, + onClick: saveChanges, + }), showBackButton: true, showCancelButton: false, }, { index: 2, formComponent: SSOConfigAuthorizeStep, - validations: [], + validations: SSOConfigAuthorizeStepValidations, stepName: 'Authorize', nextButtonConfig: placeHolderButton(), showBackButton: true, diff --git a/src/components/settings/SettingsSSOTab/UnsavedSSOChangesModal.tsx b/src/components/settings/SettingsSSOTab/UnsavedSSOChangesModal.tsx new file mode 100644 index 0000000000..7f4711347a --- /dev/null +++ b/src/components/settings/SettingsSSOTab/UnsavedSSOChangesModal.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { ModalDialog, ActionRow, Button } from '@edx/paragon'; +import { UnsavedChangesModalProps } from '../../forms/FormWorkflow'; + +const UnsavedSSOChangesModal = ({ + isOpen, + close, + exitWithoutSaving, +}: UnsavedChangesModalProps) => ( + + + Exit configuration? + + +

    Your in-progress data will not be saved.

    +

    Your SSO connection will not be active until you restart and complete the SSO configuration process.

    +
    + + + + + + +
    +); + +export default UnsavedSSOChangesModal; diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx index 1d9fe823b2..6e62cb3524 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx @@ -1,38 +1,96 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { useParams } from 'react-router-dom'; import { Alert, Form, Hyperlink, Button, Row, } from '@edx/paragon'; import { Info, Download } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; +import { createSAMLURLs } from '../utils'; +import { SSOConfigContext } from '../SSOConfigContext'; +import { setFormFieldAction } from '../../../forms/data/actions'; +import { FormFieldValidation, useFormContext } from '../../../forms/FormContext'; -const handleCheck = () => null; - -const SSOConfigAuthorizeStep = () => ( - <> -

    Authorize edX as a Service Provider

    - -

    Action required in a new window

    - Return to this window after completing the following steps in a new window to finish configuring your integration. -
    -
    -

    - 1. Download the edX Service Provider metadata as an XML file: -

    - - - - - -

    - 2. Launch a new window and upload the XML file to the list of - authorized SAML Service Providers on your Identity Provider's portal or website. -

    -
    -

    Return to this window and check the box once complete

    - - - I have authorized edX as a Service Provider - - -); +export const validations: FormFieldValidation[] = [ + { + formFieldId: 'confirmAuthorizedEdxServiceProvider', + validator: (fields) => { + const ret = !fields.confirmAuthorizedEdxServiceProvider; + return ret; + }, + }, +]; + +// TODO: Move with SSOConfigContext +type SSOProviderConfig = { + slug: string; +}; + +type SSOState = { + providerConfig?: SSOProviderConfig; +}; + +type SSOConfigContextValue = { + ssoState?: SSOState; +}; + +const SSOConfigAuthorizeStep = () => { + const configuration = getConfig(); + const { + ssoState, + } = useContext(SSOConfigContext); + const { + dispatch, + formFields, + } = useFormContext(); + const { enterpriseSlug } = useParams(); + + const idpSlug = ssoState?.providerConfig?.slug; + const learnerPortalEnabled = ssoState?.providerConfig?.slug; + + const { testLink } = createSAMLURLs({ + configuration, idpSlug, enterpriseSlug, learnerPortalEnabled, + }); + + const handleCheck = (event) => { + dispatch?.( + setFormFieldAction({ fieldId: 'confirmAuthorizedEdxServiceProvider', value: event.target.checked }), + ); + }; + + return ( + <> +

    Authorize edX as a Service Provider

    + +

    Action required in a new window

    + Return to this window after completing the following steps + in a new window to finish configuring your integration. +
    +
    +

    + 1. Download the edX Service Provider metadata as an XML file: +

    + + + + +

    + 2. + + Launch a new window + + {' '} and upload the XML file to the list of + authorized SAML Service Providers on your Identity Provider's portal or website. +

    +
    +

    Return to this window and check the box once complete

    + + + I have authorized edX as a Service Provider + + + ); +}; export default SSOConfigAuthorizeStep; diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx index 50cf5b5ba2..30e347d1ef 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx @@ -4,18 +4,60 @@ import { } from '@edx/paragon'; import ValidatedFormControl from '../../../forms/ValidatedFormControl'; +import { FormContext, FormFieldValidation, useFormContext } from '../../../forms/FormContext'; +import { urlValidation } from '../../../../utils'; + +const isSAPConfig = (fields) => fields.identityProvider === 'sap_success_factors'; + +export const validations: FormFieldValidation[] = [ + { + formFieldId: 'sapsfOauthRootUrl', + validator: (fields) => isSAPConfig(fields) && (!fields.sapsfOauthRootUrl || !urlValidation(fields.sapsfOauthRootUrl)) && 'Please enter an OAuth Root URL.', + }, + { + formFieldId: 'odataApiRootUrl', + validator: (fields) => isSAPConfig(fields) && (!fields.odataApiRootUrl || !urlValidation(fields.odataApiRootUrl)) && 'Please enter an API Root URL.', + }, + { + formFieldId: 'sapsfPrivateKey', + validator: (fields) => isSAPConfig(fields) && !fields.sapsfPrivateKey && 'Please enter a Private Key.', + }, + { + formFieldId: 'odataCompanyId', + validator: (fields) => isSAPConfig(fields) && !fields.odataCompanyId && 'Please enter a Company ID.', + }, + { + formFieldId: 'oauthUserId', + validator: (fields) => isSAPConfig(fields) && !fields.oauthUserId && 'Please enter an OAuth User ID.', + }, +]; const SSOConfigConfigureStep = () => { + const { + formFields, + }: FormContext = useFormContext(); + const usingSAP = formFields?.identityProvider === 'sap_success_factors'; + const renderBaseFields = () => ( <> -

    Enter user attributes

    + +

    Enter user attributes

    +

    Please enter the SAML user attributes from your Identity Provider. All attributes are space and case sensitive.

    + + + { { { { { ); const renderSAPFields = () => ( <> -

    Enable learner account auto-registration

    + +

    Enable learner account auto-registration

    +
    { { { { {

    Enter integration details

    -

    Set display name

    + +

    Set display name

    +
    - {/* TODO: Render SAP fields selectively once logic is in place */} - {renderBaseFields()} - {renderSAPFields()} + {usingSAP ? renderSAPFields() : renderBaseFields()}
    ); diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx index 4b66625d11..ac33ed1c18 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx @@ -1,14 +1,46 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Container, Dropzone, Form } from '@edx/paragon'; import ValidatedFormRadio from '../../../forms/ValidatedFormRadio'; import ValidatedFormControl from '../../../forms/ValidatedFormControl'; -import { FormContext, useFormContext } from '../../../forms/FormContext'; +import { FormContext, FormFieldValidation, useFormContext } from '../../../forms/FormContext'; +import { setFormFieldAction } from '../../../forms/data/actions'; +import { urlValidation } from '../../../../utils'; + +export const IDP_URL_SELECTION = 'idp_metadata_url'; +export const IDP_XML_SELECTION = 'idp_metadata_xml'; +const urlEntrySelected = (formFields) => formFields?.idpConnectOption === IDP_URL_SELECTION; +const xmlEntrySelected = (formFields) => formFields?.idpConnectOption === IDP_XML_SELECTION; + +export const validations: FormFieldValidation[] = [ + { + formFieldId: 'identityProvider', + validator: (fields) => !fields.identityProvider && 'Please select an SSO Identity Provider', + }, + { + formFieldId: 'idpConnectOption', + validator: (fields) => !fields.idpConnectOption && 'Please select a connection method', + }, + { + formFieldId: 'metadataUrl', + validator: (fields) => { + const error = urlEntrySelected(fields) && !urlValidation(fields.metadataUrl); + return error && 'Please enter an Identity Provider Metadata URL'; + }, + }, + { + formFieldId: 'metadataXml', + validator: (fields) => { + const error = !fields.metadataXml && xmlEntrySelected(fields); + return error && 'Please upload an Identity Provider Metadata XML file'; + }, + }, +]; const SSOConfigConnectStep = () => { const fiveGbInBytes = 5368709120; const ssoIdpOptions = [ - ['Microsoft Azure Active Directory (Azure AD)', 'azure_ad'], + ['Microsoft Entra ID', 'microsoft_entra_id'], ['Google Workspace', 'google_workspace'], ['Okta', 'okta'], ['OneLogin', 'one_login'], @@ -16,18 +48,25 @@ const SSOConfigConnectStep = () => { ['Other', 'other'], ]; const idpConnectOptions = [ - ['Enter identity Provider Metadata URL', 'idp_metadata_url'], - ['Upload Identity Provider Metadata XML file', 'idp_metadata_xml'], + ['Enter identity Provider Metadata URL', IDP_URL_SELECTION], + ['Upload Identity Provider Metadata XML file', IDP_XML_SELECTION], ]; const { - formFields, + formFields, dispatch, showErrors, errorMap, }: FormContext = useFormContext(); - const showUrlEntry = formFields?.idpConnectOption === 'idp_metadata_url'; - const showXmlUpload = formFields?.idpConnectOption === 'idp_metadata_xml'; + const [xmlUploadFileName, setXmlUploadFileName] = useState(''); + const showUrlEntry = urlEntrySelected(formFields); + const showXmlUpload = xmlEntrySelected(formFields); + const xmlUploadError = errorMap?.metadataXml; - // TODO: Store uploaded XML data - const onUploadXml = () => null; + const onUploadXml = ({ fileData }) => { + const blob = fileData.get('file'); + blob.text().then(xmlText => { + dispatch?.(setFormFieldAction({ fieldId: 'metadataXml', value: xmlText })); + setXmlUploadFileName(blob.name); + }); + }; return ( @@ -38,13 +77,13 @@ const SSOConfigConnectStep = () => { What is your organization's SSO Identity Provider? -

    Connect edX to your Identity Provider

    +

    Connect edX to your Identity Provider

    Select a method to connect edX to your Identity Provider { {showUrlEntry && ( { {showXmlUpload && ( - + <> + + {xmlUploadFileName && ( + + Uploaded{' '} + {xmlUploadFileName} + + )} + {showErrors && xmlUploadError && {xmlUploadError}} + )}
    diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx index 4cc7cecc93..82fbe4109e 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx @@ -2,6 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import userEvent from '@testing-library/user-event'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import Router from 'react-router'; import { Provider } from 'react-redux'; import NewSSOConfigForm from '../NewSSOConfigForm'; @@ -43,6 +44,14 @@ jest.mock('../hooks', () => { useExistingSSOConfigs: () => [[{ hehe: 'haha' }], null, true], }; }); +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useParams: jest.fn(), +})); + +// const enterUrlText = 'Find the URL in your Identity Provider portal or website.'; +const enterUrlText = 'Identity Provider Metadata URL'; +const uploadXmlText = 'Drag and drop your file here or click to upload.'; const store = getMockStore({ ...initialStore }); @@ -71,11 +80,11 @@ const contextValue = { setRefreshBool: jest.fn(), }; -const setupNewSSOStepper = () => { +const setupNewSSOStepper = (contextChanges = {}) => { features.AUTH0_SELF_SERVICE_INTEGRATION = true; return render( - + , @@ -322,8 +331,34 @@ describe('SAML Config Tab', () => { expect(screen.getByText('Next')).not.toBeDisabled(); }, []); }); - test('navigate through new sso workflow skeleton', async () => { + test('show correct metadata entry based on selection', async () => { setupNewSSOStepper(); + await waitFor(() => { + expect(getButtonElement('Next')).toBeInTheDocument(); + }, []); + + // Verify metadata selectors are hidden initially + expect(screen.queryByText(enterUrlText)).not.toBeInTheDocument(); + expect(screen.queryByText(uploadXmlText)).not.toBeInTheDocument(); + + // Verify metadata selectors appear with their respective selections + userEvent.click(screen.getByText('Enter identity Provider Metadata URL')); + await waitFor(() => { + expect(screen.queryByText(enterUrlText)).toBeInTheDocument(); + }, []); + expect(screen.queryByText(uploadXmlText)).not.toBeInTheDocument(); + + userEvent.click(screen.getByText('Upload Identity Provider Metadata XML file')); + await waitFor(() => { + expect(screen.queryByText(uploadXmlText)).toBeInTheDocument(); + }, []); + expect(screen.queryByText(enterUrlText)).not.toBeInTheDocument(); + }); + test('navigate through new non-SAP sso workflow', async () => { + setupNewSSOStepper(); + const mockCreateEnterpriseSsoOrchestrationRecord = jest.spyOn(LmsApiService, 'createEnterpriseSsoOrchestrationRecord'); + mockCreateEnterpriseSsoOrchestrationRecord.mockResolvedValue({ data: { record: 'fakeuuid', sp_metadata_url: 'https://fake.url' } }); + jest.spyOn(Router, 'useParams').mockReturnValue({ enterpriseSlug: 'testslug' }); // Connect Step await waitFor(() => { expect(getButtonElement('Next')).toBeInTheDocument(); @@ -331,6 +366,14 @@ describe('SAML Config Tab', () => { expect(screen.queryByText('New SSO integration')).toBeInTheDocument(); expect(screen.queryByText('Connect')).toBeInTheDocument(); expect(screen.queryByText('Let\'s get started')).toBeInTheDocument(); + // Click provider + userEvent.click(screen.getByText('Okta')); + // Click Enter identity Provider Metadata URL + userEvent.click(screen.getByText('Enter identity Provider Metadata URL')); + await waitFor(() => { + expect(screen.queryByText(enterUrlText)).toBeInTheDocument(); + }, []); + userEvent.type(screen.queryByText(enterUrlText), 'https://unimportant.link'); userEvent.click(getButtonElement('Next')); // Configure Step @@ -338,13 +381,24 @@ describe('SAML Config Tab', () => { expect(getButtonElement('Configure')).toBeInTheDocument(); }, []); expect(screen.queryByText('Enter integration details')).toBeInTheDocument(); + // Verify SAP field not present + expect(screen.queryByText('OAuth Root URL')).not.toBeInTheDocument(); + userEvent.click(getButtonElement('Configure')); // Authorize Step await waitFor(() => { expect(getButtonElement('Next')).toBeInTheDocument(); }, []); - expect(screen.queryByText('Authorize edX as a Service Provider')).toBeInTheDocument(); + // Click checkbox to advance + const getAuthorizedCheckbox = () => screen.queryByRole('checkbox'); + await waitFor(() => { + expect(getAuthorizedCheckbox()).toBeInTheDocument(); + }, []); + userEvent.click(getAuthorizedCheckbox()); + await waitFor(() => { + expect(getAuthorizedCheckbox()).toBeChecked(); + }, []); userEvent.click(getButtonElement('Next')); // Confirm and Test Step @@ -353,81 +407,100 @@ describe('SAML Config Tab', () => { }, []); expect(screen.queryByText('Wait for SSO configuration confirmation')).toBeInTheDocument(); }); - test('show correct metadata entry based on selection', async () => { + test('cancel out of new SSO workflow', async () => { setupNewSSOStepper(); + // Connect Step Select an option to trigger cancel modal + userEvent.click(screen.getByText('Okta')); + userEvent.click(getButtonElement('Cancel')); await waitFor(() => { - expect(getButtonElement('Next')).toBeInTheDocument(); + expect(getButtonElement('Exit')).toBeInTheDocument(); }, []); + userEvent.click(getButtonElement('Exit')); - const enterUrlText = 'Find the URL in your Identity Provider portal or website.'; - const uploadXmlText = 'Drag and drop your file here or click to upload.'; - - // Verify metadata selectors are hidden initially - expect(screen.queryByText(enterUrlText)).not.toBeInTheDocument(); - expect(screen.queryByText(uploadXmlText)).not.toBeInTheDocument(); - - // Verify metadata selectors appear with their respective selections - userEvent.click(screen.getByText('Enter identity Provider Metadata URL')); await waitFor(() => { - expect(screen.queryByText(enterUrlText)).toBeInTheDocument(); + expect( + screen.queryByText( + 'Connect to a SAML identity provider for single sign-on' + + ' to allow quick access to your organization\'s learning catalog.', + ), + ).toBeInTheDocument(); }, []); - expect(screen.queryByText(uploadXmlText)).not.toBeInTheDocument(); - - userEvent.click(screen.getByText('Upload Identity Provider Metadata XML file')); + }); + test('new SSO workflow load existing metadata url config', async () => { + const testMetadataUrl = 'http://test.metadata'; + const existingConfigContextValue = { + ssoState: { + providerConfig: { + uuid: 123, + metadataUrl: testMetadataUrl, + }, + }, + }; + setupNewSSOStepper(existingConfigContextValue); + // Connect Step with metadata url selected await waitFor(() => { - expect(screen.queryByText(uploadXmlText)).toBeInTheDocument(); + expect( + screen.queryByText( + 'Find the URL in your Identity Provider portal or website.', + ), + ).toBeInTheDocument(); }, []); - expect(screen.queryByText(enterUrlText)).not.toBeInTheDocument(); + screen.queryByText(testMetadataUrl); }); - test('back button shown on pages after first page', async () => { - const getBackButton = () => getButtonElement('Back'); + // TODO: Test case where we go SAP route + test('navigate through new SAP sso workflow', async () => { setupNewSSOStepper(); + const mockCreateEnterpriseSsoOrchestrationRecord = jest.spyOn(LmsApiService, 'createEnterpriseSsoOrchestrationRecord'); + mockCreateEnterpriseSsoOrchestrationRecord.mockResolvedValue({ data: { record: 'fakeuuid', sp_metadata_url: 'https://fake.url' } }); + jest.spyOn(Router, 'useParams').mockReturnValue({ enterpriseSlug: 'testslug' }); // Connect Step await waitFor(() => { expect(getButtonElement('Next')).toBeInTheDocument(); }, []); - expect(screen.queryByRole('button', { name: 'Back' })).not.toBeInTheDocument(); + expect(screen.queryByText('New SSO integration')).toBeInTheDocument(); + expect(screen.queryByText('Connect')).toBeInTheDocument(); + expect(screen.queryByText('Let\'s get started')).toBeInTheDocument(); + // Click SAP provider + userEvent.click(screen.getByText('SAP SuccessFactors')); + // Click Enter identity Provider Metadata URL + userEvent.click(screen.getByText('Enter identity Provider Metadata URL')); + await waitFor(() => { + expect(screen.queryByText(enterUrlText)).toBeInTheDocument(); + }, []); + userEvent.type(screen.queryByText(enterUrlText), 'https://unimportant.link'); userEvent.click(getButtonElement('Next')); // Configure Step await waitFor(() => { - expect(getButtonElement('Configure')).toBeInTheDocument(); + expect(screen.queryByText('OAuth Root URL')).toBeInTheDocument(); }, []); - expect(getBackButton()).toBeInTheDocument(); + // Verify Configure does not advance until fields are filled out + userEvent.click(getButtonElement('Configure')); + expect(screen.queryByText('OAuth Root URL')).toBeInTheDocument(); + const fieldEntries = [ + ['OAuth Root URL', 'https://test'], + ['API Root URL', 'https://test'], + ['Company ID', 'test'], + ['Private Key', 'test'], + ['OAuth User ID', 'test'], + ]; + fieldEntries.forEach(([fieldIdText, value]) => { + userEvent.type(screen.queryByText(fieldIdText), value); + }); userEvent.click(getButtonElement('Configure')); - - // Authorize Step await waitFor(() => { expect(getButtonElement('Next')).toBeInTheDocument(); }, []); - expect(getBackButton()).toBeInTheDocument(); - userEvent.click(getButtonElement('Next')); - - // Back from Confirm and Test Step - await waitFor(() => { - expect(getButtonElement('Finish')).toBeInTheDocument(); - }, []); - userEvent.click(getBackButton()); - await waitFor(() => { - expect(screen.queryByText('Authorize edX as a Service Provider')).toBeInTheDocument(); - }, []); }); test('cancel out of new SSO workflow', async () => { setupNewSSOStepper(); - // Connect Step - await waitFor(() => { - expect(getButtonElement('Cancel')).toBeInTheDocument(); - }, []); + // Connect Step Select an option to trigger cancel modal + userEvent.click(screen.getByText('Okta')); userEvent.click(getButtonElement('Cancel')); await waitFor(() => { - expect(getButtonElement('Cancel')).toBeInTheDocument(); - }, []); - - await waitFor(() => { - const exitButton = getButtonElement('Exit without saving'); - expect(exitButton).toBeInTheDocument(); - userEvent.click(exitButton); + expect(getButtonElement('Exit')).toBeInTheDocument(); }, []); + userEvent.click(getButtonElement('Exit')); await waitFor(() => { expect( From 179524d429846034a06655ae69a776f5642555d3 Mon Sep 17 00:00:00 2001 From: Marlon Keating <322346+marlonkeating@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:44:54 -0800 Subject: [PATCH 062/124] fix: disable creating new SSO config while configuring prior config (#1084) --- .../settings/SettingsSSOTab/NewExistingSSOConfigs.jsx | 5 ++--- src/components/settings/SettingsSSOTab/index.jsx | 6 +++++- src/components/settings/SettingsSSOTab/utils.js | 9 ++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx index 6474ef6a59..9ac1d82113 100644 --- a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx +++ b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx @@ -11,6 +11,7 @@ import { connect } from 'react-redux'; import LmsApiService from '../../../data/services/LmsApiService'; import NewSSOConfigAlerts from './NewSSOConfigAlerts'; import NewSSOConfigCard from './NewSSOConfigCard'; +import { isInProgressConfig } from './utils'; const FRESH_CONFIG_POLLING_INTERVAL = 30000; const UPDATED_CONFIG_POLLING_INTERVAL = 2000; @@ -66,9 +67,7 @@ const NewExistingSSOConfigs = ({ useEffect(() => { const [active, inactive] = _.partition(configs, config => config.active); - const inProgress = configs.filter( - config => (config.submitted_at && !config.configured_at) || (config.configured_at < config.submitted_at), - ); + const inProgress = configs.filter(isInProgressConfig); const untested = configs.filter(config => !config.validated_at); const live = configs.filter( config => (config.validated_at && config.active && config.validated_at > config.configured_at), diff --git a/src/components/settings/SettingsSSOTab/index.jsx b/src/components/settings/SettingsSSOTab/index.jsx index b1181c6817..a1f5599415 100644 --- a/src/components/settings/SettingsSSOTab/index.jsx +++ b/src/components/settings/SettingsSSOTab/index.jsx @@ -13,6 +13,7 @@ import NewSSOConfigForm from './NewSSOConfigForm'; import { SSOConfigContext, SSOConfigContextProvider } from './SSOConfigContext'; import LmsApiService from '../../../data/services/LmsApiService'; import { features } from '../../../config'; +import { isInProgressConfig } from './utils'; const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { const { @@ -51,6 +52,8 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { }, [AUTH0_SELF_SERVICE_INTEGRATION, existingConfigs, setHasSSOConfig]); if (AUTH0_SELF_SERVICE_INTEGRATION) { + const newButtonVisible = existingConfigs?.length > 0 && (providerConfig === null); + const newButtonDisabled = existingConfigs.some(isInProgressConfig); return (
    {

    Single Sign-On (SSO) Integrations

    - {existingConfigs?.length > 0 && (providerConfig === null) && ( + {newButtonVisible && ( diff --git a/src/components/settings/SettingsSSOTab/utils.js b/src/components/settings/SettingsSSOTab/utils.js index d57a8d185c..e9fd44a082 100644 --- a/src/components/settings/SettingsSSOTab/utils.js +++ b/src/components/settings/SettingsSSOTab/utils.js @@ -26,4 +26,11 @@ function createSAMLURLs({ return { testLink, spMetadataLink }; } -export { updateSamlProviderData, deleteSamlProviderData, createSAMLURLs }; +function isInProgressConfig(config) { + return (config.submitted_at && !config.configured_at) + || config.configured_at < config.submitted_at; +} + +export { + updateSamlProviderData, deleteSamlProviderData, createSAMLURLs, isInProgressConfig, +}; From 16373b9f4612796843d4c6bc5d46814335bdb2b2 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Tue, 7 Nov 2023 14:19:04 -0500 Subject: [PATCH 063/124] feat: assignment error AlertModal variants (#1082) --- .../AssignmentRowActionTableCell.jsx | 2 +- .../cards/BaseCourseCard.jsx | 2 +- .../cards/CourseCard.test.jsx | 115 +++++++++++++++++- .../CreateAllocationErrorAlertModals.jsx | 84 +++++++++++++ .../cards/NewAssignmentModalButton.jsx | 35 +++++- .../cards/data/constants.js | 9 ++ .../cards/data/index.js | 3 + .../cards/data/utils.js | 9 ++ .../ContentNotInCatalogErrorAlertModal.jsx | 46 +++++++ .../NotEnoughBalanceAlertModal.jsx | 54 ++++++++ .../status-modals/SystemErrorAlertModal.jsx | 47 +++++++ .../tests/CatalogSearchResults.test.jsx | 14 +++ 12 files changed, 408 insertions(+), 12 deletions(-) create mode 100644 src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx create mode 100644 src/components/learner-credit-management/cards/data/constants.js create mode 100644 src/components/learner-credit-management/cards/data/index.js create mode 100644 src/components/learner-credit-management/cards/data/utils.js create mode 100644 src/components/learner-credit-management/cards/status-modals/ContentNotInCatalogErrorAlertModal.jsx create mode 100644 src/components/learner-credit-management/cards/status-modals/NotEnoughBalanceAlertModal.jsx create mode 100644 src/components/learner-credit-management/cards/status-modals/SystemErrorAlertModal.jsx diff --git a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx index a180598d31..eec4589cdd 100644 --- a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx @@ -9,7 +9,7 @@ const AssignmentRowActionTableCell = ({ row }) => { const isLearnerStateWaiting = row.original.learnerState === 'waiting'; const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; return ( - + {isLearnerStateWaiting && ( { + const mockAllocateContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'allocateContentAssignments'); + + // Helper function to find the assignment error modal after failed allocation attempt + const getAssignmentErrorModal = () => within(screen.queryAllByRole('dialog')[1]); + + // Helper function to simulate clicking on "Try again" in error modal to retry allocation + const simulateClickErrorModalTryAgain = async (modalTitle, assignmentErrorModal) => { + const tryAgainCTA = getButtonElement('Try again', { screenOverride: assignmentErrorModal }); + expect(tryAgainCTA).toBeInTheDocument(); + userEvent.click(tryAgainCTA); + await waitFor(() => { + // Verify modal closes + expect(assignmentErrorModal.queryByText(modalTitle)).not.toBeInTheDocument(); + }); + expect(mockAllocateContentAssignments).toHaveBeenCalledTimes(2); + }; + + // Helper function to simulate clicking on "Exit and discard changes" in error modal to close ALL modals + const simulateClickErrorModalExit = async (assignmentErrorModal) => { + const exitCTA = getButtonElement('Exit and discard changes', { screenOverride: assignmentErrorModal }); + userEvent.click(exitCTA); + await waitFor(() => { + // Verify all modals close (error modal + assignment modal) + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + expect(mockAllocateContentAssignments).toHaveBeenCalledTimes(1); + }; + beforeEach(() => { useSubsidyAccessPolicy.mockReturnValue({ data: mockSubsidyAccessPolicy, @@ -176,13 +204,59 @@ describe('Course card works as expected', () => { }); test.each([ - { shouldSubmitAssignments: true, hasAllocationException: true }, + { + shouldSubmitAssignments: true, + hasAllocationException: true, + errorReason: 'content_not_in_catalog', + }, + { + shouldSubmitAssignments: true, + hasAllocationException: true, + errorReason: 'not_enough_value_in_subsidy', + shouldRetryAfterError: false, + }, + { + shouldSubmitAssignments: true, + hasAllocationException: true, + errorReason: 'not_enough_value_in_subsidy', + shouldRetryAfterError: true, + }, + { shouldSubmitAssignments: true, + hasAllocationException: true, + errorReason: 'policy_spend_limit_reached', + shouldRetryAfterError: false, + }, + { shouldSubmitAssignments: true, + hasAllocationException: true, + errorReason: 'policy_spend_limit_reached', + shouldRetryAfterError: true, + }, + { shouldSubmitAssignments: true, + hasAllocationException: true, + errorReason: null, + shouldRetryAfterError: false, + }, + { shouldSubmitAssignments: true, + hasAllocationException: true, + errorReason: null, + shouldRetryAfterError: true, + }, { shouldSubmitAssignments: true, hasAllocationException: false }, { shouldSubmitAssignments: false, hasAllocationException: false }, - ])('opens assignment modal, submits assignments successfully (%s)', async ({ shouldSubmitAssignments, hasAllocationException }) => { - const mockAllocateContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'allocateContentAssignments'); + ])('opens assignment modal, submits assignments successfully (%s)', async ({ + shouldSubmitAssignments, + hasAllocationException, + errorReason, + shouldRetryAfterError, + }) => { if (hasAllocationException) { - mockAllocateContentAssignments.mockRejectedValue(new Error('oops')); + // mock Axios error + mockAllocateContentAssignments.mockRejectedValue({ + customAttributes: { + httpErrorStatus: errorReason ? 422 : 500, + httpErrorResponseData: JSON.stringify([{ reason: errorReason }]), + }, + }); } else { mockAllocateContentAssignments.mockResolvedValue({ data: { @@ -286,9 +360,40 @@ describe('Course card works as expected', () => { }), ); + // Verify error states if (hasAllocationException) { - // Verify error state expect(getButtonElement('Try again', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'false'); + + // Assert the correct error modal is displayed + if (errorReason === 'content_not_in_catalog') { + const assignmentErrorModal = getAssignmentErrorModal(); + expect(assignmentErrorModal.getByText(`This course is not in your ${mockSubsidyAccessPolicy.displayName} budget's catalog`)).toBeInTheDocument(); + const exitCTA = getButtonElement('Exit', { screenOverride: assignmentErrorModal }); + userEvent.click(exitCTA); + await waitFor(() => { + // Verify all modals close (error modal + assignment modal) + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + } else if (['not_enough_value_in_subsidy', 'policy_spend_limit_reached'].includes(errorReason)) { + const assignmentErrorModal = getAssignmentErrorModal(); + const errorModalTitle = 'Not enough balance'; + expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); + if (shouldRetryAfterError) { + await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); + } else { + await simulateClickErrorModalExit(assignmentErrorModal); + } + } else { + const assignmentErrorModal = getAssignmentErrorModal(); + const errorModalTitle = 'Something went wrong'; + expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); + if (shouldRetryAfterError) { + await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); + } else { + await simulateClickErrorModalExit(assignmentErrorModal); + } + } + } else { // Verify success state expect(mockInvalidateQueries).toHaveBeenCalledTimes(1); diff --git a/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx new file mode 100644 index 0000000000..82d14eba94 --- /dev/null +++ b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx @@ -0,0 +1,84 @@ +import React, { useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useToggle } from '@edx/paragon'; +import SystemErrorAlertModal from './status-modals/SystemErrorAlertModal'; +import ContentNotInCatalogErrorAlertModal from './status-modals/ContentNotInCatalogErrorAlertModal'; +import NotEnoughBalanceAlertModal from './status-modals/NotEnoughBalanceAlertModal'; + +const CreateAllocationErrorAlertModals = ({ + errorReason, + retry, + closeAssignmentModal, +}) => { + const [isCatalogError, openCatalogErrorModal, closeCatalogErrorModal] = useToggle(false); + const [isSystemError, openSystemErrorModal, closeSystemErrorModal] = useToggle(false); + const [isBalanceError, openBalanceErrorModal, closeBalanceErrorModal] = useToggle(false); + + /** + * Close all error modals. + */ + const closeAllErrorModals = useCallback(() => { + const closeFns = [closeCatalogErrorModal, closeBalanceErrorModal, closeSystemErrorModal]; + closeFns.forEach((closeFn) => { + closeFn(); + }); + }, [closeCatalogErrorModal, closeBalanceErrorModal, closeSystemErrorModal]); + + /** + * Retry the original action that caused the error. + */ + const handleErrorRetry = () => { + retry(); + closeAllErrorModals(); + }; + + /** + * Whenever the `errorReason` prop changes, open the associated error modal to + * surface the error messaging to the user. If no `errorReason` exists, close + * any error modals that may be previously open. + */ + useEffect(() => { + // Always ensure any open error modal is closed before opening a new one, OR when + // there is error reason. + closeAllErrorModals(); + + // Open specific error modal based on error reason. + if (errorReason === 'content_not_in_catalog') { + openCatalogErrorModal(); + } else if (['not_enough_value_in_subsidy', 'policy_spend_limit_reached'].includes(errorReason)) { + openBalanceErrorModal(); + } else if (errorReason) { + openSystemErrorModal(); + } + }, [errorReason, closeAllErrorModals, openCatalogErrorModal, openBalanceErrorModal, openSystemErrorModal]); + + return ( + <> + + + + + ); +}; + +CreateAllocationErrorAlertModals.propTypes = { + closeAssignmentModal: PropTypes.func.isRequired, + retry: PropTypes.func.isRequired, + errorReason: PropTypes.string, +}; + +export default CreateAllocationErrorAlertModals; diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index e23dbb5273..708d9972e0 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -15,6 +15,7 @@ import { snakeCaseObject } from '@edx/frontend-platform/utils'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys, useBudgetId } from '../data'; +import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals'; const useAllocateContentAssignments = () => useMutation({ mutationFn: async ({ @@ -28,15 +29,20 @@ const NewAssignmentModalButton = ({ course, children }) => { const routeMatch = useRouteMatch(); const queryClient = useQueryClient(); const { subsidyAccessPolicyId } = useBudgetId(); - const [isOpen, open, close] = useToggle(false); const [learnerEmails, setLearnerEmails] = useState([]); const [assignButtonState, setAssignButtonState] = useState('default'); + const [createAssignmentsErrorReason, setCreateAssignmentsErrorReason] = useState(); const { mutate } = useAllocateContentAssignments(); const pathToActivityTab = generatePath(routeMatch.path, { budgetId: subsidyAccessPolicyId, activeTabKey: 'activity' }); + const handleCloseAssignmentModal = () => { + close(); + setAssignButtonState('default'); + }; + const handleAllocateContentAssignments = () => { const payload = snakeCaseObject({ contentPriceCents: course.normalizedMetadata.contentPrice * 100, // Convert to USD cents @@ -48,16 +54,27 @@ const NewAssignmentModalButton = ({ course, children }) => { payload, }; setAssignButtonState('pending'); + setCreateAssignmentsErrorReason(null); mutate(mutationArgs, { onSuccess: () => { setAssignButtonState('complete'); queryClient.invalidateQueries({ queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), }); - close(); + handleCloseAssignmentModal(); history.push(pathToActivityTab); }, - onError: () => { + onError: (err) => { + const { + httpErrorStatus, + httpErrorResponseData, + } = err.customAttributes; + if (httpErrorStatus === 422) { + const responseData = JSON.parse(httpErrorResponseData); + setCreateAssignmentsErrorReason(responseData[0].reason); + } else { + setCreateAssignmentsErrorReason('system_error'); + } setAssignButtonState('error'); }, }); @@ -70,7 +87,7 @@ const NewAssignmentModalButton = ({ course, children }) => { className="bg-light-200 text-left" title="Assign this course" isOpen={isOpen} - onClose={close} + onClose={handleCloseAssignmentModal} footerNode={( + + )} + > +

    + This course is not included in the catalog for your {budgetDisplayName}. Please try again with another course. +

    + + ); +}; + +ContentNotInCatalogErrorAlertModal.propTypes = { ...commonErrorAlertModalPropTypes }; + +export default ContentNotInCatalogErrorAlertModal; diff --git a/src/components/learner-credit-management/cards/status-modals/NotEnoughBalanceAlertModal.jsx b/src/components/learner-credit-management/cards/status-modals/NotEnoughBalanceAlertModal.jsx new file mode 100644 index 0000000000..fe480d7320 --- /dev/null +++ b/src/components/learner-credit-management/cards/status-modals/NotEnoughBalanceAlertModal.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { AlertModal, ActionRow, Button } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; + +import { commonErrorAlertModalPropTypes, getBudgetDisplayName } from '../data'; +import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from '../../data'; + +const NotEnoughBalanceAlertModal = ({ + isErrorModalOpen, + closeErrorModal, + closeAssignmentModal, + retry, +}) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + const budgetDisplayName = getBudgetDisplayName(subsidyAccessPolicy); + + const handleClose = () => { + closeErrorModal(); + closeAssignmentModal(); + }; + + return ( + + + + + )} + > +

    + The total assignment cost exceeds your {`${budgetDisplayName}'s`} available balance + of {formatPrice(subsidyAccessPolicy.aggregates.spendAvailableUsd)}. Please + remove learners and try again. +

    +
    + ); +}; + +NotEnoughBalanceAlertModal.propTypes = { + ...commonErrorAlertModalPropTypes, + retry: PropTypes.func.isRequired, +}; + +export default NotEnoughBalanceAlertModal; diff --git a/src/components/learner-credit-management/cards/status-modals/SystemErrorAlertModal.jsx b/src/components/learner-credit-management/cards/status-modals/SystemErrorAlertModal.jsx new file mode 100644 index 0000000000..76ff98c0ff --- /dev/null +++ b/src/components/learner-credit-management/cards/status-modals/SystemErrorAlertModal.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ActionRow, AlertModal, Button } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; + +import { commonErrorAlertModalPropTypes } from '../data'; + +const SystemErrorAlertModal = ({ + isErrorModalOpen, + closeErrorModal, + closeAssignmentModal, + retry, +}) => { + const handleClose = () => { + closeErrorModal(); + closeAssignmentModal(); + }; + + return ( + + + + + )} + > +

    + We're sorry. Something went wrong behind the scenes. Please + try again, or reach out to customer support for help. +

    +
    + ); +}; + +SystemErrorAlertModal.propTypes = { + ...commonErrorAlertModalPropTypes, + retry: PropTypes.func.isRequired, +}; + +export default SystemErrorAlertModal; diff --git a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx index 32807bbfdf..9d5ac8df71 100644 --- a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx @@ -12,6 +12,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { BaseCatalogSearchResults } from '../search/CatalogSearchResults'; import { CONTENT_TYPE_COURSE } from '../data/constants'; import { queryClient } from '../../test/testUtils'; +import { useSubsidyAccessPolicy } from '../data'; // Mocking this connected component so as not to have to mock the algolia Api const PAGINATE_ME = 'PAGINATE ME :)'; @@ -24,6 +25,19 @@ jest.mock('react-instantsearch-dom', () => ({ Index: () =>
    Popular Courses
    , })); +jest.mock('../data', () => ({ + ...jest.requireActual('../data'), + useSubsidyAccessPolicy: jest.fn().mockReturnValue({ + data: { + uuid: 'test-uuid', + displayName: 'Test Budget', + aggregates: { + spendAvailableUsd: 100, + }, + }, + }), +})); + const DEFAULT_SEARCH_CONTEXT_VALUE = { refinements: {} }; const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); From d518a4d0536cba8cd8d1175894676107376d66ff Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Tue, 7 Nov 2023 14:53:21 -0500 Subject: [PATCH 064/124] feat: display success toast on assignment allocation (#1083) --- .../BudgetDetailPageWrapper.jsx | 29 ++++- .../cards/CourseCard.test.jsx | 37 +++++-- .../CreateAllocationErrorAlertModals.jsx | 6 +- .../cards/NewAssignmentModalButton.jsx | 5 +- .../data/hooks/index.js | 1 + ...seSuccessfulAssignmentToastContextValue.js | 35 ++++++ .../tests/BudgetDetailPageWrapper.test.jsx | 100 ++++++++++++++++++ .../tests/CatalogSearchResults.test.jsx | 12 ++- 8 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js create mode 100644 src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx diff --git a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx index 9d6d95943d..b7ae27b754 100644 --- a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx @@ -1,12 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { Container } from '@edx/paragon'; +import { Container, Toast } from '@edx/paragon'; import Hero from '../Hero'; +import { useSuccessfulAssignmentToastContextValue } from './data'; const PAGE_TITLE = 'Learner Credit Management'; +export const BudgetDetailPageContext = React.createContext(); + const BudgetDetailPageWrapper = ({ subsidyAccessPolicy, includeHero, @@ -16,14 +19,34 @@ const BudgetDetailPageWrapper = ({ // similar to the display name logic for budgets on the overview page route. const budgetDisplayName = subsidyAccessPolicy?.displayName || 'Overview'; const helmetPageTitle = budgetDisplayName ? `${budgetDisplayName} - ${PAGE_TITLE}` : PAGE_TITLE; + + const successfulAssignmentToastContextValue = useSuccessfulAssignmentToastContextValue(); + const { + isSuccessfulAssignmentAllocationToastOpen, + successfulAssignmentAllocationToastMessage, + closeToastForAssignmentAllocation, + } = successfulAssignmentToastContextValue; + return ( - <> + {includeHero && } {children} - + {/** + Successful assignment allocation Toast notification. It is rendered here to guarantee that the + Toast component will not be unmounted when the user programmatically navigates to the "Activity" + tab, which will unmount the course cards that rendered the assignment modal. Thus, the Toast must + be rendered within the component tree that's common to both the "Activity" and "Overview" tabs. + */} + + {successfulAssignmentAllocationToastMessage} + + ); }; diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/CourseCard.test.jsx index d845c3273a..247147b3ae 100644 --- a/src/components/learner-credit-management/cards/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.test.jsx @@ -20,6 +20,7 @@ import { import { getButtonElement, queryClient } from '../../test/testUtils'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; +import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; jest.mock('@tanstack/react-query', () => ({ ...jest.requireActual('@tanstack/react-query'), @@ -94,8 +95,17 @@ const mockSubsidyAccessPolicy = { }; const mockLearnerEmails = ['hello@example.com', 'world@example.com']; +const mockDisplaySuccessfulAssignmentToast = jest.fn(); +const defaultBudgetDetailPageContextValue = { + isSuccessfulAssignmentAllocationToastOpen: false, + totalLearnersAssigned: undefined, + displayToastForAssignmentAllocation: mockDisplaySuccessfulAssignmentToast, + closeToastForAssignmentAllocation: jest.fn(), +}; + const CourseCardWrapper = ({ initialState = initialStoreState, + budgetDetailPageContextValue = defaultBudgetDetailPageContextValue, ...rest }) => { const store = getMockStore({ ...initialState }); @@ -109,7 +119,9 @@ const CourseCardWrapper = ({ config: { ENTERPRISE_LEARNER_PORTAL_URL: mockLearnerPortal }, }} > - + + + @@ -221,22 +233,26 @@ describe('Course card works as expected', () => { errorReason: 'not_enough_value_in_subsidy', shouldRetryAfterError: true, }, - { shouldSubmitAssignments: true, + { + shouldSubmitAssignments: true, hasAllocationException: true, errorReason: 'policy_spend_limit_reached', shouldRetryAfterError: false, }, - { shouldSubmitAssignments: true, + { + shouldSubmitAssignments: true, hasAllocationException: true, errorReason: 'policy_spend_limit_reached', shouldRetryAfterError: true, }, - { shouldSubmitAssignments: true, + { + shouldSubmitAssignments: true, hasAllocationException: true, errorReason: null, shouldRetryAfterError: false, }, - { shouldSubmitAssignments: true, + { + shouldSubmitAssignments: true, hasAllocationException: true, errorReason: null, shouldRetryAfterError: true, @@ -363,7 +379,7 @@ describe('Course card works as expected', () => { // Verify error states if (hasAllocationException) { expect(getButtonElement('Try again', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'false'); - + // Assert the correct error modal is displayed if (errorReason === 'content_not_in_catalog') { const assignmentErrorModal = getAssignmentErrorModal(); @@ -393,7 +409,6 @@ describe('Course card works as expected', () => { await simulateClickErrorModalExit(assignmentErrorModal); } } - } else { // Verify success state expect(mockInvalidateQueries).toHaveBeenCalledTimes(1); @@ -401,9 +416,15 @@ describe('Course card works as expected', () => { queryKey: learnerCreditManagementQueryKeys.budget(mockSubsidyAccessPolicy.uuid), }); expect(getButtonElement('Assigned', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'true'); - // Verify modal closes await waitFor(() => { + // Verify all modals close (error modal + assignment modal) expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + // Verify toast notification was displayed + expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledTimes(1); + expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledWith({ + totalLearnersAssigned: mockLearnerEmails.length, + }); }); } } else { diff --git a/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx index 82d14eba94..dc5c92ebe6 100644 --- a/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx +++ b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx @@ -13,9 +13,9 @@ const CreateAllocationErrorAlertModals = ({ const [isCatalogError, openCatalogErrorModal, closeCatalogErrorModal] = useToggle(false); const [isSystemError, openSystemErrorModal, closeSystemErrorModal] = useToggle(false); const [isBalanceError, openBalanceErrorModal, closeBalanceErrorModal] = useToggle(false); - + /** - * Close all error modals. + * Helper function to close all error modals. */ const closeAllErrorModals = useCallback(() => { const closeFns = [closeCatalogErrorModal, closeBalanceErrorModal, closeSystemErrorModal]; @@ -25,7 +25,7 @@ const CreateAllocationErrorAlertModals = ({ }, [closeCatalogErrorModal, closeBalanceErrorModal, closeSystemErrorModal]); /** - * Retry the original action that caused the error. + * Retry the original action that caused the error and close all error modals. */ const handleErrorRetry = () => { retry(); diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 708d9972e0..fccc05b501 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { useRouteMatch, useHistory, generatePath } from 'react-router-dom'; import { @@ -16,6 +16,7 @@ import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys, useBudgetId } from '../data'; import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals'; +import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; const useAllocateContentAssignments = () => useMutation({ mutationFn: async ({ @@ -33,6 +34,7 @@ const NewAssignmentModalButton = ({ course, children }) => { const [learnerEmails, setLearnerEmails] = useState([]); const [assignButtonState, setAssignButtonState] = useState('default'); const [createAssignmentsErrorReason, setCreateAssignmentsErrorReason] = useState(); + const { displayToastForAssignmentAllocation } = useContext(BudgetDetailPageContext); const { mutate } = useAllocateContentAssignments(); @@ -62,6 +64,7 @@ const NewAssignmentModalButton = ({ course, children }) => { queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), }); handleCloseAssignmentModal(); + displayToastForAssignmentAllocation({ totalLearnersAssigned: learnerEmails.length }); history.push(pathToActivityTab); }, onError: (err) => { diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index b7c35f509f..6da548a12b 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -7,3 +7,4 @@ export { default as useSubsidyAccessPolicy } from './useSubsidyAccessPolicy'; export { default as usePathToCatalogTab } from './usePathToCatalogTab'; export { default as useBudgetDetailActivityOverview } from './useBudgetDetailActivityOverview'; export { default as useIsLargeOrGreater } from './useIsLargeOrGreater'; +export { default as useSuccessfulAssignmentToastContextValue } from './useSuccessfulAssignmentToastContextValue'; diff --git a/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js b/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js new file mode 100644 index 0000000000..8d52284152 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js @@ -0,0 +1,35 @@ +import { useCallback, useMemo, useState } from 'react'; + +const useSuccessfulAssignmentToastContextValue = () => { + const [isToastOpen, setIsToastOpen] = useState(false); + const [learnersAssignedCount, setLearnersAssignedCount] = useState(); + + const handleDisplayToast = useCallback(({ totalLearnersAssigned }) => { + setIsToastOpen(true); + setLearnersAssignedCount(totalLearnersAssigned); + }, []); + + const handleCloseToast = useCallback(() => { + setIsToastOpen(false); + }, []); + + const successfulAssignmentAllocationToastMessage = `Course successfully assigned to ${learnersAssignedCount} ${learnersAssignedCount === 1 ? 'learner' : 'learners'}.`; + + const successfulAssignmentToastContextValue = useMemo(() => ({ + isSuccessfulAssignmentAllocationToastOpen: isToastOpen, + displayToastForAssignmentAllocation: handleDisplayToast, + closeToastForAssignmentAllocation: handleCloseToast, + totalLearnersAssigned: learnersAssignedCount, + successfulAssignmentAllocationToastMessage, + }), [ + isToastOpen, + handleDisplayToast, + handleCloseToast, + learnersAssignedCount, + successfulAssignmentAllocationToastMessage, + ]); + + return successfulAssignmentToastContextValue; +}; + +export default useSuccessfulAssignmentToastContextValue; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx new file mode 100644 index 0000000000..7207e8f8b4 --- /dev/null +++ b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx @@ -0,0 +1,100 @@ +import { useContext } from 'react'; +import { Button } from '@edx/paragon'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import '@testing-library/jest-dom/extend-expect'; + +import BudgetDetailPageWrapper, { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; +import { getButtonElement } from '../../test/testUtils'; + +const mockStore = configureMockStore([thunk]); +const getMockStore = store => mockStore(store); +const enterpriseSlug = 'test-enterprise'; +const enterpriseUUID = '1234'; +const defaultStoreState = { + portalConfiguration: { + enterpriseId: enterpriseUUID, + enterpriseSlug, + enableLearnerPortal: true, + enterpriseFeatures: { + topDownAssignmentRealTimeLcm: true, + }, + }, +}; + +const MockBudgetDetailPageWrapper = ({ + initialStoreState = defaultStoreState, + children, +}) => { + const store = getMockStore(initialStoreState); + return ( + + + + {children} + + + + ); +}; + +describe('', () => { + it('should render its children and display hero by default', () => { + render(
    hello world
    ); + // Verify children are rendered + expect(screen.getByText('hello world')).toBeInTheDocument(); + // Verify Hero is rendered with the expected page title + expect(screen.getByText('Learner Credit Management')).toBeInTheDocument(); + }); + + it.each([ + { totalLearnersAssigned: 1, expectedLearnerString: 'learner' }, + { totalLearnersAssigned: 2, expectedLearnerString: 'learners' }, + ])('should render Toast notification for successful assignment allocation (%s)', async ({ + totalLearnersAssigned, + expectedLearnerString, + }) => { + const ToastContextController = () => { + const { + displayToastForAssignmentAllocation, + closeToastForAssignmentAllocation, + } = useContext(BudgetDetailPageContext); + + const handleDisplayToast = () => { + displayToastForAssignmentAllocation({ totalLearnersAssigned }); + }; + + const handleCloseToast = () => { + closeToastForAssignmentAllocation(); + }; + + return ( +
    + + +
    + ); + }; + render(); + + const expectedToastMessage = `Course successfully assigned to ${totalLearnersAssigned} ${expectedLearnerString}.`; + + // Open Toast notification + userEvent.click(getButtonElement('Open Toast')); + + // Verify Toast notification is rendered + expect(screen.getByText(expectedToastMessage)).toBeInTheDocument(); + + // Close Toast notification + userEvent.click(getButtonElement('Close Toast')); + + // Verify Toast notification is no longer rendered + await waitFor(() => { + expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx index 9d5ac8df71..0143e8d5ba 100644 --- a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx @@ -12,7 +12,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { BaseCatalogSearchResults } from '../search/CatalogSearchResults'; import { CONTENT_TYPE_COURSE } from '../data/constants'; import { queryClient } from '../../test/testUtils'; -import { useSubsidyAccessPolicy } from '../data'; +import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; // Mocking this connected component so as not to have to mock the algolia Api const PAGINATE_ME = 'PAGINATE ME :)'; @@ -170,10 +170,18 @@ describe('Main Catalogs view works as expected', () => { }); test('all courses rendered when search results available', async () => { + const budgetDetailPageContextValue = { + isSuccessfulAssignmentAllocationToastOpen: false, + totalLearnersAssigned: undefined, + displayToastForAssignmentAllocation: jest.fn(), + closeToastForAssignmentAllocation: jest.fn(), + }; renderWithRouter( - + + + , , From ac1eb5cda12ee77faee1ca871a2b84d3934aaa65 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Thu, 9 Nov 2023 08:03:44 -0500 Subject: [PATCH 065/124] feat: add filtering to status column on assigned table, ensure assigned table has ordering (#1085) --- .../BudgetAssignmentsTable.jsx | 175 ++++++---- .../data/hooks/useBudgetContentAssignments.js | 36 ++ .../hooks/useBudgetContentAssignments.test.js | 152 +++++++++ .../tests/BudgetDetailPage.test.jsx | 307 +++++++++++++++--- 4 files changed, 559 insertions(+), 111 deletions(-) diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index cfc6603800..69821935f9 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { DataTable } from '@edx/paragon'; +import { DataTable, CheckboxFilter } from '@edx/paragon'; import TableTextFilter from './TableTextFilter'; import CustomDataTableEmptyState from './CustomDataTableEmptyState'; import AssignmentDetailsTableCell from './AssignmentDetailsTableCell'; @@ -14,80 +14,119 @@ import AssignmentsTableRefreshAction from './AssignmentsTableRefreshAction'; const FilterStatus = (rest) => ; +const getLearnerStateDisplayName = (learnerState) => { + if (learnerState === 'notifying') { + return 'Notifying learner'; + } + if (learnerState === 'waiting') { + return 'Waiting for learner'; + } + if (learnerState === 'failed') { + return 'Failed'; + } + + return undefined; +}; + const BudgetAssignmentsTable = ({ isLoading, tableData, fetchTableData, -}) => ( - `-${formatPrice(row.original.contentQuantity / 100)}`, - disableFilters: true, - }, - { - Header: 'Status', - Cell: AssignmentStatusTableCell, - disableFilters: true, - }, - { - Header: 'Recent action', - Cell: AssignmentRecentActionTableCell, - disableFilters: true, - }, - ]} - additionalColumns={[ - { - Header: '', - Cell: AssignmentRowActionTableCell, - id: 'action', - }, - ]} - tableActions={[ - , - ]} - initialTableOptions={{ - getRowId: row => row?.uuid?.toString(), - }} - initialState={{ - pageSize: PAGE_SIZE, - pageIndex: DEFAULT_PAGE, - sortBy: [], - filters: [], - }} - fetchData={fetchTableData} - data={tableData?.results || []} - itemCount={tableData?.count || 0} - pageCount={tableData?.numPages || 1} - EmptyTableComponent={CustomDataTableEmptyState} - bulkActions={[ - , - , - ]} - /> -); +}) => { + const statusFilterChoices = tableData.learnerStateCounts + .filter(({ learnerState }) => !!getLearnerStateDisplayName(learnerState)) + .map(({ learnerState, count }) => ({ + name: getLearnerStateDisplayName(learnerState), + number: count, + value: learnerState, + })); + + return ( + `-${formatPrice(row.original.contentQuantity / 100)}`, + disableFilters: true, + }, + { + Header: 'Status', + accessor: 'learnerState', + Cell: AssignmentStatusTableCell, + Filter: CheckboxFilter, + filter: 'includesValue', + filterChoices: statusFilterChoices, + }, + { + Header: 'Recent action', + accessor: 'recentAction', + Cell: AssignmentRecentActionTableCell, + disableFilters: true, + }, + ]} + additionalColumns={[ + { + Header: '', + Cell: AssignmentRowActionTableCell, + id: 'action', + }, + ]} + tableActions={[ + , + ]} + initialTableOptions={{ + getRowId: row => row?.uuid?.toString(), + }} + initialState={{ + pageSize: PAGE_SIZE, + pageIndex: DEFAULT_PAGE, + sortBy: [{ + id: 'recentAction', + desc: true, + }], + filters: [], + }} + fetchData={fetchTableData} + data={tableData.results || []} + itemCount={tableData.count || 0} + pageCount={tableData.numPages || 1} + EmptyTableComponent={CustomDataTableEmptyState} + bulkActions={[ + , + , + ]} + /> + ); +}; BudgetAssignmentsTable.propTypes = { isLoading: PropTypes.bool.isRequired, - tableData: PropTypes.shape().isRequired, + tableData: PropTypes.shape({ + results: PropTypes.arrayOf(PropTypes.shape()), + learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({ + learnerState: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + })).isRequired, + count: PropTypes.number.isRequired, + numPages: PropTypes.number.isRequired, + }).isRequired, fetchTableData: PropTypes.func.isRequired, }; diff --git a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js index 45031d520e..8bd33e9a07 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js +++ b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js @@ -9,6 +9,7 @@ const initialContentAssignmentsState = { count: 0, numPages: 0, currentPage: 1, + learnerStateCounts: [], }; const applyFiltersToOptions = (filters, options) => { @@ -19,6 +20,40 @@ const applyFiltersToOptions = (filters, options) => { if (searchQuery) { Object.assign(options, { search: searchQuery }); } + const learnerStateFilter = filters.find(filter => filter.id === 'learnerState')?.value; + if (learnerStateFilter) { + Object.assign(options, { learnerState: learnerStateFilter.join(',') }); + } +}; + +const applySortByToOptions = (sortBy, options) => { + if (!sortBy || sortBy.length === 0) { + return; + } + const apiFieldsForColumnAccessor = { + recentAction: { key: 'recent_action_time' }, + learnerState: { key: 'learner_state_sort_order' }, + amount: { key: 'content_quantity', isReversed: true }, + }; + const orderingStrings = sortBy.map(({ id, desc }) => { + const apiFieldForColumnAccessor = apiFieldsForColumnAccessor[id]; + if (!apiFieldForColumnAccessor) { + return undefined; + } + const isApiFieldOrderingReversed = apiFieldForColumnAccessor.isReversed; + const apiFieldKey = apiFieldForColumnAccessor.key; + // Determine whether the API field ordering should be reversed based on the column accessor. This is + // necessary because the content_quantity field is a negative number, but if the column is sorted in a + // descending order, users would likely expect the larger contenr quantity to be at the top of the list, + // which is technically the smaller number since its negative. + if (isApiFieldOrderingReversed) { + return desc ? apiFieldKey : `-${apiFieldKey}`; + } + return desc ? `-${apiFieldKey}` : apiFieldKey; + }).filter(orderingString => !!orderingString); + Object.assign(options, { + ordering: orderingStrings.join(','), + }); }; const useBudgetContentAssignments = ({ @@ -40,6 +75,7 @@ const useBudgetContentAssignments = ({ pageSize: args.pageSize, }; applyFiltersToOptions(args.filters, options); + applySortByToOptions(args.sortBy, options); const assignmentsResponse = await EnterpriseAccessApiService.listContentAssignments( assignmentConfigurationUUID, options, diff --git a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js index a0715f22ef..27d7aef092 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js +++ b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js @@ -128,4 +128,156 @@ describe('useBudgetContentAssignments', () => { }, ); }); + + it.each([ + { + filters: [ + { + id: 'learnerState', + value: ['waiting'], + }, + ], + selectedLearnerStateQueryParam: 'waiting', + }, + { + filters: [ + { + id: 'learnerState', + value: ['waiting', 'notifying'], + }, + ], + selectedLearnerStateQueryParam: 'waiting,notifying', + }, + ])('handles learner state (status) filtering (%s)', async ({ filters, selectedLearnerStateQueryParam }) => { + const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ + assignmentConfigurationUUID: '123', + isEnabled: true, + })); + const { fetchContentAssignments } = result.current; + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + mockListContentAssignments.mockResolvedValue({ + data: { + results: [ + { + id: 1, + name: 'test', + }, + ], + count: 1, + numPages: 1, + currentPage: 1, + }, + }); + await fetchContentAssignments({ + pageIndex: 0, + pageSize: 10, + filters, + }); + + await waitForNextUpdate(); + + expect(mockListContentAssignments).toHaveBeenCalledWith( + '123', + { + page: 1, + pageSize: 10, + learnerState: selectedLearnerStateQueryParam, + }, + ); + }); + + it.each([ + { + sortBy: [ + { + id: 'learnerState', + desc: false, + }, + ], + orderingQueryParam: 'learner_state_sort_order', + }, + { + sortBy: [ + { + id: 'learnerState', + desc: true, + }, + ], + orderingQueryParam: '-learner_state_sort_order', + }, + { + sortBy: [ + { + id: 'recentAction', + desc: false, + }, + ], + orderingQueryParam: 'recent_action_time', + }, + { + sortBy: [ + { + id: 'recentAction', + desc: true, + }, + ], + orderingQueryParam: '-recent_action_time', + }, + { + sortBy: [ + { + id: 'amount', + desc: false, + }, + ], + // Ordering is reversed for `content_quantity` field + orderingQueryParam: '-content_quantity', + }, + { + sortBy: [ + { + id: 'amount', + desc: true, + }, + ], + // Ordering is reversed for `content_quantity` field + orderingQueryParam: 'content_quantity', + }, + ])('handles ordering on appropriate columns (%s)', async ({ sortBy, orderingQueryParam }) => { + const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ + assignmentConfigurationUUID: '123', + isEnabled: true, + })); + const { fetchContentAssignments } = result.current; + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + mockListContentAssignments.mockResolvedValue({ + data: { + results: [ + { + id: 1, + name: 'test', + }, + ], + count: 1, + numPages: 1, + currentPage: 1, + }, + }); + await fetchContentAssignments({ + pageIndex: 0, + pageSize: 10, + sortBy, + }); + + await waitForNextUpdate(); + + expect(mockListContentAssignments).toHaveBeenCalledWith( + '123', + { + page: 1, + pageSize: 10, + ordering: orderingQueryParam, + }, + ); + }); }); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 5f3beec3a5..6f2574d25f 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -29,7 +29,7 @@ import { mockSubsidyAccessPolicyUUID, mockEnterpriseOfferId, } from '../data/tests/constants'; -import { queryClient } from '../../test/testUtils'; +import { getButtonElement, queryClient } from '../../test/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -95,6 +95,18 @@ const mockFailedLinkedLearnerAction = { actionType: 'learner_linked', errorReason: 'internal_api_error', }; +const mockLearnerContentAssignment = { + uuid: 'test-uuid', + learnerEmail: mockLearnerEmail, + contentKey: mockCourseKey, + contentTitle: mockContentTitle, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulLinkedLearnerAction, mockSuccessfulNotifiedAction], + errorReason: null, +}; + const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; @@ -313,19 +325,8 @@ describe('', () => { isLoading: false, contentAssignments: { count: 1, - results: [ - { - uuid: 'test-uuid', - learnerEmail: mockLearnerEmail, - contentKey: mockCourseKey, - contentTitle: mockContentTitle, - contentQuantity: -19900, - learnerState: 'waiting', - recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, - actions: [mockSuccessfulNotifiedAction], - errorReason: null, - }, - ], + results: [mockLearnerContentAssignment], + learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], numPages: 1, currentPage: 1, }, @@ -349,20 +350,258 @@ describe('', () => { expect(assignedSection.getByText('Waiting for learner')).toBeInTheDocument(); expect(assignedSection.getByText(`Assigned: ${formatDate('2023-10-27')}`)).toBeInTheDocument(); - // Verify "Refresh" behavior - const expectedRefreshArgs = { + const expectedTableFetchDataArgs = { pageIndex: DEFAULT_PAGE, pageSize: PAGE_SIZE, filters: [], - sortBy: [], + sortBy: [{ id: 'recentAction', desc: true }], // default table sort order }; expect(mockFetchContentAssignments).toHaveBeenCalledTimes(1); // called once on initial render - expect(mockFetchContentAssignments).toHaveBeenCalledWith(expect.objectContaining(expectedRefreshArgs)); + expect(mockFetchContentAssignments).toHaveBeenCalledWith(expect.objectContaining(expectedTableFetchDataArgs)); + + // Verify "Refresh" behavior const refreshCTA = assignedSection.getByText('Refresh', { selector: 'button' }); expect(refreshCTA).toBeInTheDocument(); userEvent.click(refreshCTA); expect(mockFetchContentAssignments).toHaveBeenCalledTimes(2); // should be called again on refresh - expect(mockFetchContentAssignments).toHaveBeenLastCalledWith(expect.objectContaining(expectedRefreshArgs)); + expect(mockFetchContentAssignments).toHaveBeenLastCalledWith(expect.objectContaining(expectedTableFetchDataArgs)); + }); + + it.each([ + { sortByColumnHeader: 'Amount', expectedSortBy: [{ id: 'amount', desc: false }] }, + ])('renders sortable assigned table data', async ({ sortByColumnHeader, expectedSortBy }) => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + const mockFetchContentAssignments = jest.fn(); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [mockLearnerContentAssignment], + learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: mockFetchContentAssignments, + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); + renderWithRouter(); + + const assignedSection = within(screen.getByText('Assigned').closest('section')); + const expectedDefaultTableFetchDataArgs = { + pageIndex: DEFAULT_PAGE, + pageSize: PAGE_SIZE, + filters: [], + sortBy: [{ id: 'recentAction', desc: true }], // default table sort order + }; + const expectedDefaultTableFetchDataArgsAfterSort = { + ...expectedDefaultTableFetchDataArgs, + sortBy: expectedSortBy, + }; + + expect(mockFetchContentAssignments).toHaveBeenCalledTimes(1); // called once on initial render + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedDefaultTableFetchDataArgs), + ); + + // Verify amount column sort + const amountColumnHeader = assignedSection.getByText(sortByColumnHeader); + userEvent.click(amountColumnHeader); + + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedDefaultTableFetchDataArgsAfterSort), + ); + }); + + it.each([ + { + filterBy: { + field: 'status', + value: ['waiting'], + }, + expectedFilters: [{ id: 'learnerState', value: ['waiting'] }], + }, + { + filterBy: { + field: 'search', + value: mockLearnerEmail, + }, + expectedFilters: [{ id: 'assignmentDetails', value: mockLearnerEmail }], + }, + ])('renders filterable assigned table data (%s)', async ({ + filterBy, + expectedFilters, + }) => { + const { field, value } = filterBy; + + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + const mockFetchContentAssignments = jest.fn(); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [mockLearnerContentAssignment], + learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: mockFetchContentAssignments, + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); + renderWithRouter(); + + const assignedSection = within(screen.getByText('Assigned').closest('section')); + const expectedDefaultTableFetchDataArgs = { + pageIndex: DEFAULT_PAGE, + pageSize: PAGE_SIZE, + filters: [], + sortBy: [{ id: 'recentAction', desc: true }], // default table sort order + }; + const expectedTableFetchDataArgsAfterFilter = { + ...expectedDefaultTableFetchDataArgs, + filters: expectedFilters, + }; + expect(mockFetchContentAssignments).toHaveBeenCalledTimes(1); // called once on initial render + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedDefaultTableFetchDataArgs), + ); + + if (field === 'status') { + const filtersButton = getButtonElement('Filters', { screenOverride: assignedSection }); + userEvent.click(filtersButton); + const filtersDropdown = screen.getByRole('group', { name: 'Status' }); + const filtersDropdownContainer = within(filtersDropdown); + if (value.includes('waiting')) { + const waitingForLearnerOption = filtersDropdownContainer.getByLabelText('Waiting for learner 1', { exact: false }); + expect(waitingForLearnerOption).toBeInTheDocument(); + userEvent.click(waitingForLearnerOption); + + await waitFor(() => { + expect(waitingForLearnerOption).toBeChecked(); + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedTableFetchDataArgsAfterFilter), + ); + }); + } + } + + if (field === 'search') { + const assignmentDetailsInputField = assignedSection.getByLabelText('Search by assignment details'); + userEvent.type(assignmentDetailsInputField, value); + + await waitFor(() => { + expect(assignmentDetailsInputField).toHaveValue(value); + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedTableFetchDataArgsAfterFilter), + ); + }); + } + }); + + it.each([ + { + columnHeader: 'Amount', + columnId: 'amount', + }, + ])('renders sortable assigned table data (%s)', async ({ columnHeader, columnId }) => { + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + const mockFetchContentAssignments = jest.fn(); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [mockLearnerContentAssignment], + learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: mockFetchContentAssignments, + }); + useOfferRedemptions.mockReturnValue({ + isLoading: false, + offerRedemptions: mockEmptyOfferRedemptions, + fetchOfferRedemptions: jest.fn(), + }); + renderWithRouter(); + + const assignedSection = within(screen.getByText('Assigned').closest('section')); + const expectedDefaultTableFetchDataArgs = { + pageIndex: DEFAULT_PAGE, + pageSize: PAGE_SIZE, + filters: [], + sortBy: [{ id: 'recentAction', desc: true }], // default table sort order + }; + const expectedTableFetchDataArgsAfterSortAsc = { + ...expectedDefaultTableFetchDataArgs, + sortBy: [{ id: columnId, desc: false }], + }; + const expectedTableFetchDataArgsAfterSortDesc = { + ...expectedDefaultTableFetchDataArgs, + sortBy: [{ id: columnId, desc: true }], + }; + expect(mockFetchContentAssignments).toHaveBeenCalledTimes(1); // called once on initial render + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedDefaultTableFetchDataArgs), + ); + + const orderedColumnHeader = assignedSection.getByText(columnHeader); + userEvent.click(orderedColumnHeader); + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedTableFetchDataArgsAfterSortAsc), + ); + userEvent.click(orderedColumnHeader); + expect(mockFetchContentAssignments).toHaveBeenCalledWith( + expect.objectContaining(expectedTableFetchDataArgsAfterSortDesc), + ); }); it('renders with assigned table data "View Course" hyperlink default when content title is null', () => { @@ -385,19 +624,8 @@ describe('', () => { isLoading: false, contentAssignments: { count: 1, - results: [ - { - uuid: 'test-uuid', - learnerEmail: mockLearnerEmail, - contentKey: mockCourseKey, - contentTitle: null, - contentQuantity: -19900, - learnerState: 'waiting', - recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, - actions: [mockSuccessfulNotifiedAction], - errorReason: null, - }, - ], + results: [{ ...mockLearnerContentAssignment, contentTitle: null }], + learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], numPages: 1, currentPage: 1, }, @@ -513,16 +741,14 @@ describe('', () => { count: 1, results: [ { - uuid: 'test-uuid', + ...mockLearnerContentAssignment, learnerEmail: hasLearnerEmail ? mockLearnerEmail : null, - contentKey: mockCourseKey, - contentQuantity: -19900, learnerState, - recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, actions, errorReason, }, ], + learnerStateCounts: [{ learnerState, count: 1 }], numPages: 1, currentPage: 1, }, @@ -792,16 +1018,11 @@ describe('', () => { count: 1, results: [ { - uuid: 'test-uuid', - contentKey: mockCourseKey, - contentQuantity: -19900, + ...mockLearnerContentAssignment, learnerState, - recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, - actions: [], - errorReason: null, - state: 'allocated', }, ], + learnerStateCounts: [{ learnerState, count: 1 }], numPages: 1, currentPage: 1, }, From ca9b6649e1d10448856ff83ffe63b86091e9607a Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Tue, 7 Nov 2023 20:24:35 +0000 Subject: [PATCH 066/124] feat: implementing gated top down allocation table subsidy service api usage --- .../BudgetDetailRedemptions.jsx | 14 +- .../data/hooks/useOfferRedemptions.js | 55 ++++-- .../data/hooks/useOfferRedemptions.test.js | 89 ---------- .../data/hooks/useOfferRedemptions.test.jsx | 165 ++++++++++++++++++ .../data/tests/constants.js | 2 + .../learner-credit-management/data/utils.js | 10 ++ .../tests/BudgetDetailPage.test.jsx | 4 +- .../services/EnterpriseSubsidyApiService.js | 20 ++- .../tests/EnterpriseSubsidyApiService.test.js | 11 +- 9 files changed, 255 insertions(+), 115 deletions(-) delete mode 100644 src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.js create mode 100644 src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx diff --git a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx index 67fd14f087..0ea2fb766a 100644 --- a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx +++ b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx @@ -5,14 +5,18 @@ import { connect } from 'react-redux'; import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; import { useBudgetId, useOfferRedemptions } from './data'; -const BudgetDetailRedemptions = ({ enterpriseUUID }) => { +const BudgetDetailRedemptions = ({ enterpriseFeatures, enterpriseUUID }) => { const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId(); const { isLoading, offerRedemptions, fetchOfferRedemptions, - } = useOfferRedemptions(enterpriseUUID, enterpriseOfferId, subsidyAccessPolicyId); - + } = useOfferRedemptions( + enterpriseUUID, + enterpriseOfferId, + subsidyAccessPolicyId, + enterpriseFeatures.topDownAssignmentRealTimeLcm, + ); return (

    Spent

    @@ -30,11 +34,15 @@ const BudgetDetailRedemptions = ({ enterpriseUUID }) => { }; const mapStateToProps = state => ({ + enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, enterpriseUUID: state.portalConfiguration.enterpriseId, }); BudgetDetailRedemptions.propTypes = { enterpriseUUID: PropTypes.string.isRequired, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool, + }).isRequired, }; export default connect(mapStateToProps)(BudgetDetailRedemptions); diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js index 356c05404d..c5f824b8a1 100644 --- a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js +++ b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js @@ -10,8 +10,10 @@ import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import debounce from 'lodash.debounce'; import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; +import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; import { API_FIELDS_BY_TABLE_COLUMN_ACCESSOR } from '../constants'; -import { transformUtilizationTableResults } from '../utils'; +import { transformUtilizationTableResults, transformUtilizationTableSubsidyTransactionResults } from '../utils'; +import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; const applySortByToOptions = (sortBy, options) => { const orderingStrings = sortBy.map(({ id, desc }) => { @@ -29,19 +31,26 @@ const applySortByToOptions = (sortBy, options) => { }); }; -const applyFiltersToOptions = (filters, options) => { +const applyFiltersToOptions = (filters, options, shouldFetchSubsidyTransactions = false) => { const courseProductLineSearchQuery = filters?.find(filter => filter.id === 'courseProductLine')?.value; - const searchQuery = filters?.find(filter => filter.id.toLowerCase() === 'enrollment details')?.value; + const searchQuery = filters?.find(filter => filter.id === 'enrollmentDetails')?.value; if (courseProductLineSearchQuery) { Object.assign(options, { courseProductLine: courseProductLineSearchQuery }); } if (searchQuery) { - Object.assign(options, { searchAll: searchQuery }); + const searchParams = {}; + searchParams[shouldFetchSubsidyTransactions ? 'search' : 'searchAll'] = searchQuery; + Object.assign(options, searchParams); } }; -const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) => { +const useOfferRedemptions = ( + enterpriseUUID, + offerId = null, + budgetId = null, + shouldFetchSubsidyTransactions = false, +) => { const shouldTrackFetchEvents = useRef(false); const [isLoading, setIsLoading] = useState(true); const [offerRedemptions, setOfferRedemptions] = useState({ @@ -49,6 +58,7 @@ const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) => pageCount: 0, results: [], }); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(budgetId); const fetchOfferRedemptions = useCallback((args) => { const fetch = async () => { @@ -69,14 +79,26 @@ const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) => applySortByToOptions(args.sortBy, options); } if (args.filters?.length > 0) { - applyFiltersToOptions(args.filters, options); + applyFiltersToOptions(args.filters, options, shouldFetchSubsidyTransactions); } - const response = await EnterpriseDataApiService.fetchCourseEnrollments( - enterpriseUUID, - options, - ); - const data = camelCaseObject(response.data); - const transformedTableResults = transformUtilizationTableResults(data.results); + let data; + let transformedTableResults; + if (budgetId && shouldFetchSubsidyTransactions) { + const response = await SubsidyApiService.fetchCustomerTransactions( + subsidyAccessPolicy?.subsidyUuid, + options, + ); + data = camelCaseObject(response.data); + transformedTableResults = transformUtilizationTableSubsidyTransactionResults(data.results); + } else { + const response = await EnterpriseDataApiService.fetchCourseEnrollments( + enterpriseUUID, + options, + ); + data = camelCaseObject(response.data); + transformedTableResults = transformUtilizationTableResults(data.results); + } + setOfferRedemptions({ itemCount: data.count, pageCount: data.numPages, @@ -104,7 +126,14 @@ const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) => if (offerId || budgetId) { fetch(); } - }, [enterpriseUUID, offerId, budgetId, shouldTrackFetchEvents]); + }, [ + enterpriseUUID, + offerId, + budgetId, + shouldTrackFetchEvents, + shouldFetchSubsidyTransactions, + subsidyAccessPolicy?.subsidyUuid, + ]); const debouncedFetchOfferRedemptions = useMemo(() => debounce(fetchOfferRedemptions, 300), [fetchOfferRedemptions]); diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.js b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.js deleted file mode 100644 index cfe7affd2d..0000000000 --- a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.js +++ /dev/null @@ -1,89 +0,0 @@ -import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { camelCaseObject } from '@edx/frontend-platform/utils'; - -import useOfferRedemptions from './useOfferRedemptions'; -import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; - -const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; -const TEST_ENTERPRISE_OFFER_ID = 1; - -const mockOfferEnrollments = [{ - user_email: 'edx@example.com', - course_title: 'Test Course Title', - course_list_price: '100.00', - enrollment_date: '2022-01-01', -}]; - -const mockOfferEnrollmentsResponse = { - count: 100, - current_page: 1, - num_pages: 5, - results: mockOfferEnrollments, -}; - -const mockEnterpriseOffer = { - id: TEST_ENTERPRISE_OFFER_ID, -}; - -jest.mock('../../../../data/services/EnterpriseDataApiService'); - -describe('useOfferRedemptions', () => { - it('should fetch enrollment/redemptions metadata for enterprise offer', async () => { - EnterpriseDataApiService.fetchCourseEnrollments.mockResolvedValueOnce({ data: mockOfferEnrollmentsResponse }); - const budgetId = 'test-budget-id'; - const { result, waitForNextUpdate } = renderHook(() => useOfferRedemptions( - TEST_ENTERPRISE_UUID, - mockEnterpriseOffer.id, - budgetId, - )); - - expect(result.current).toMatchObject({ - offerRedemptions: { - itemCount: 0, - pageCount: 0, - results: [], - }, - isLoading: true, - fetchOfferRedemptions: expect.any(Function), - }); - act(() => { - result.current.fetchOfferRedemptions({ - pageIndex: 0, // `DataTable` uses zero-based indexing - pageSize: 20, - sortBy: [ - { id: 'enrollmentDate', desc: true }, - ], - filters: [ - { id: 'Enrollment Details', value: mockOfferEnrollments[0].user_email }, - ], - }); - }); - - await waitForNextUpdate(); - - const expectedApiOptions = { - page: 1, - pageSize: 20, - offerId: mockEnterpriseOffer.id, - ordering: '-enrollment_date', // default sort order - searchAll: mockOfferEnrollments[0].user_email, - ignoreNullCourseListPrice: true, - budgetId, - }; - expect(EnterpriseDataApiService.fetchCourseEnrollments).toHaveBeenCalledWith( - TEST_ENTERPRISE_UUID, - expectedApiOptions, - ); - expect(result.current).toMatchObject({ - offerRedemptions: { - itemCount: 100, - pageCount: 5, - results: camelCaseObject(mockOfferEnrollments), - }, - isLoading: false, - fetchOfferRedemptions: expect.any(Function), - }); - - expect(expectedApiOptions.budgetId).toBe(budgetId); - }); -}); diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx new file mode 100644 index 0000000000..9b5d1c3c9f --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx @@ -0,0 +1,165 @@ +import { QueryClientProvider } from '@tanstack/react-query'; +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; + +import useOfferRedemptions from './useOfferRedemptions'; +import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; +import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; +import { queryClient } from '../../../test/testUtils'; + +const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; +const TEST_ENTERPRISE_OFFER_ID = 1; +const subsidyUuid = 'test-subsidy-uuid'; +const courseTitle = 'Test Course Title'; +const userEmail = 'edx@example.com'; + +const mockOfferEnrollments = [{ + user_email: userEmail, + course_title: courseTitle, + course_list_price: '100.00', + enrollment_date: '2022-01-01', +}]; + +const mockOfferEnrollmentsResponse = { + count: 100, + current_page: 1, + num_pages: 5, + results: mockOfferEnrollments, +}; + +const mockSubsidyTransactionResponse = { + count: 100, + current_page: 1, + num_pages: 5, + results: [{ + uuid: subsidyUuid, + state: 'committed', + idempotency_key: '5d00d319-fe46-41f7-b14e-966534da9f72', + lms_user_id: 999, + lms_user_email: userEmail, + content_key: 'course-v1:edX+test+course.1', + content_title: courseTitle, + quantity: -1000, + unit: 'usd_cents', + }], +}; + +const mockEnterpriseOffer = { + id: TEST_ENTERPRISE_OFFER_ID, +}; + +jest.mock('./useSubsidyAccessPolicy'); +jest.mock('../../../../data/services/EnterpriseDataApiService'); +jest.mock('../../../../data/services/EnterpriseSubsidyApiService'); + +const wrapper = ({ children }) => ( + {children} +); + +describe('useOfferRedemptions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it.each([ + { + budgetId: 'test-budget-id', + offerId: undefined, + shouldFetchSubsidyTransactions: true, + }, + { + budgetId: 'test-budget-id', + offerId: undefined, + shouldFetchSubsidyTransactions: false, + }, + { + budgetId: undefined, + offerId: mockEnterpriseOffer.id, + shouldFetchSubsidyTransactions: false, + }, + ])('should fetch enrollment/redemptions metadata for enterprise offer', async ({ + budgetId, + offerId, + shouldFetchSubsidyTransactions, + }) => { + EnterpriseDataApiService.fetchCourseEnrollments.mockResolvedValueOnce({ data: mockOfferEnrollmentsResponse }); + SubsidyApiService.fetchCustomerTransactions.mockResolvedValueOnce({ data: mockSubsidyTransactionResponse }); + useSubsidyAccessPolicy.mockReturnValue({ data: { subsidyUuid } }); + + const { result, waitForNextUpdate } = renderHook( + () => useOfferRedemptions(TEST_ENTERPRISE_UUID, offerId, budgetId, shouldFetchSubsidyTransactions), + { wrapper }, + ); + + expect(result.current).toMatchObject({ + offerRedemptions: { + itemCount: 0, + pageCount: 0, + results: [], + }, + isLoading: true, + fetchOfferRedemptions: expect.any(Function), + }); + act(() => { + result.current.fetchOfferRedemptions({ + pageIndex: 0, // `DataTable` uses zero-based indexing + pageSize: 20, + sortBy: [ + { id: 'enrollmentDate', desc: true }, + ], + filters: [ + { id: 'enrollmentDetails', value: mockOfferEnrollments[0].user_email }, + ], + }); + }); + + await waitForNextUpdate(); + + if (budgetId && shouldFetchSubsidyTransactions) { + const expectedApiOptions = { + page: 1, + pageSize: 20, + offerId, + ordering: '-enrollment_date', // default sort order + search: mockOfferEnrollments[0].user_email, + ignoreNullCourseListPrice: true, + budgetId, + }; + expect(SubsidyApiService.fetchCustomerTransactions).toHaveBeenCalledWith( + subsidyUuid, + expectedApiOptions, + ); + } else { + const expectedApiOptions = { + page: 1, + pageSize: 20, + offerId, + ordering: '-enrollment_date', // default sort order + searchAll: mockOfferEnrollments[0].user_email, + ignoreNullCourseListPrice: true, + budgetId, + }; + expect(EnterpriseDataApiService.fetchCourseEnrollments).toHaveBeenCalledWith( + TEST_ENTERPRISE_UUID, + expectedApiOptions, + ); + } + + const mockExpectedResultsObj = shouldFetchSubsidyTransactions ? [{ + courseListPrice: 10, + courseTitle, + userEmail, + }] : camelCaseObject(mockOfferEnrollments); + + expect(result.current).toMatchObject({ + offerRedemptions: { + itemCount: 100, + pageCount: 5, + results: mockExpectedResultsObj, + }, + isLoading: false, + fetchOfferRedemptions: expect.any(Function), + }); + }); +}); diff --git a/src/components/learner-credit-management/data/tests/constants.js b/src/components/learner-credit-management/data/tests/constants.js index a72d643f63..285a0e7e82 100644 --- a/src/components/learner-credit-management/data/tests/constants.js +++ b/src/components/learner-credit-management/data/tests/constants.js @@ -12,6 +12,7 @@ export const mockAssignableSubsidyAccessPolicy = { spendAvailableUsd: 10000, }, isAssignable: true, + subsidyUuid: 'mock-subsidy-uuid', }; export const mockPerLearnerSpendLimitSubsidyAccessPolicy = { @@ -22,4 +23,5 @@ export const mockPerLearnerSpendLimitSubsidyAccessPolicy = { spendAvailableUsd: 10000, }, isAssignable: false, + subsidyUuid: 'mock-subsidy-uuid', }; diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 61122ad402..5780e28eb2 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -95,6 +95,16 @@ export const transformUtilizationTableResults = results => results.map(result => courseKey: result.courseKey, })); +export const transformUtilizationTableSubsidyTransactionResults = results => results.map(result => ({ + created: result.created, + enterpriseEnrollmentId: result.fulfillmentIdentifier, + userEmail: result.lmsUserEmail, + courseTitle: result.contentTitle, + courseListPrice: result.unit === 'usd_cents' ? -1 * (result.quantity / 100) : -1 * results.quantity, + uuid: result.uuid, + courseKey: result.contentKey, +})); + /** * Gets appropriate color variant for the annotated progress bar. * diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 6f2574d25f..d8729c61b5 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -209,11 +209,11 @@ describe('', () => { it.each([ { budgetId: mockEnterpriseOfferId, - expectedUseOfferRedemptionsArgs: [enterpriseUUID, mockEnterpriseOfferId, null], + expectedUseOfferRedemptionsArgs: [enterpriseUUID, mockEnterpriseOfferId, null, true], }, { budgetId: mockSubsidyAccessPolicyUUID, - expectedUseOfferRedemptionsArgs: [enterpriseUUID, null, mockSubsidyAccessPolicyUUID], + expectedUseOfferRedemptionsArgs: [enterpriseUUID, null, mockSubsidyAccessPolicyUUID, true], }, ])('displays spend table in "Activity" tab with empty results (%s)', async ({ budgetId, diff --git a/src/data/services/EnterpriseSubsidyApiService.js b/src/data/services/EnterpriseSubsidyApiService.js index f1769424bf..84550170fa 100644 --- a/src/data/services/EnterpriseSubsidyApiService.js +++ b/src/data/services/EnterpriseSubsidyApiService.js @@ -4,19 +4,29 @@ import { snakeCaseObject } from '@edx/frontend-platform'; import { configuration } from '../../config'; class SubsidyApiService { - static baseUrl = `${configuration.ENTERPRISE_SUBSIDY_BASE_URL}/api/v1`; + static baseUrl = `${configuration.ENTERPRISE_SUBSIDY_BASE_URL}/api`; + + static baseUrlV1 = `${this.baseUrl}/v1`; + + static baseUrlV2 = `${this.baseUrl}/v2`; static apiClient = getAuthenticatedHttpClient; + static fetchCustomerTransactions(subsidyUuid, options = {}) { + const queryParams = new URLSearchParams({ + ...snakeCaseObject(options), + }); + const url = `${SubsidyApiService.baseUrlV2}/subsidies/${subsidyUuid}/transactions/?${queryParams.toString()}`; + return SubsidyApiService.apiClient().get(url); + } + static getSubsidyByCustomerUUID(uuid, options = {}) { const queryParams = new URLSearchParams({ enterprise_customer_uuid: uuid, ...snakeCaseObject(options), }); - const url = `${SubsidyApiService.baseUrl}/subsidies/?${queryParams.toString()}`; - return SubsidyApiService.apiClient({ - useCache: configuration.USE_API_CACHE, - }).get(url, { clearCacheEntry: true }); + const url = `${SubsidyApiService.baseUrlV1}/subsidies/?${queryParams.toString()}`; + return SubsidyApiService.apiClient().get(url); } } diff --git a/src/data/services/tests/EnterpriseSubsidyApiService.test.js b/src/data/services/tests/EnterpriseSubsidyApiService.test.js index 18797031f6..6c894bc1f8 100644 --- a/src/data/services/tests/EnterpriseSubsidyApiService.test.js +++ b/src/data/services/tests/EnterpriseSubsidyApiService.test.js @@ -15,11 +15,16 @@ describe('EnterpriseSubsidyApiService', () => { beforeEach(() => { jest.clearAllMocks(); }); - + test('fetchCustomerTransactions calls the API to fetch transactions by enterprise subsidy', () => { + const mockSubsidyUUID = 'test-subsidy-uuid'; + const expectedUrl = `${SubsidyApiService.baseUrlV2}/subsidies/${mockSubsidyUUID}/transactions/?`; + SubsidyApiService.fetchCustomerTransactions(mockSubsidyUUID); + expect(axios.get).toBeCalledWith(expectedUrl); + }); test('getSubsidyByCustomerUUID calls the API to fetch subsides by enterprise customer UUID', () => { const mockCustomerUUID = 'test-customer-uuid'; - const expectedUrl = `${SubsidyApiService.baseUrl}/subsidies/?enterprise_customer_uuid=${mockCustomerUUID}`; + const expectedUrl = `${SubsidyApiService.baseUrlV1}/subsidies/?enterprise_customer_uuid=${mockCustomerUUID}`; SubsidyApiService.getSubsidyByCustomerUUID(mockCustomerUUID); - expect(axios.get).toBeCalledWith(expectedUrl, { clearCacheEntry: true }); + expect(axios.get).toBeCalledWith(expectedUrl); }); }); From 9157b00d6aa867707691524cb436ad132bde14d0 Mon Sep 17 00:00:00 2001 From: Marlon Keating <322346+marlonkeating@users.noreply.github.com> Date: Thu, 9 Nov 2023 09:06:22 -0800 Subject: [PATCH 067/124] feat: Add loading spinner to FormWorkflow next button (#1086) fix: unit tests chore: remove commented out line --- src/components/forms/FormWorkflow.tsx | 21 +++++++++++++++++---- src/components/forms/_FormWorkflow.scss | 4 ++++ 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 src/components/forms/_FormWorkflow.scss diff --git a/src/components/forms/FormWorkflow.tsx b/src/components/forms/FormWorkflow.tsx index 80288c075a..d17d131548 100644 --- a/src/components/forms/FormWorkflow.tsx +++ b/src/components/forms/FormWorkflow.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import type { Dispatch } from 'react'; import { - ActionRow, Button, FullscreenModal, Stepper, useToggle, + ActionRow, Button, FullscreenModal, Spinner, Stepper, useToggle, } from '@edx/paragon'; import { Launch } from '@edx/paragon/icons'; @@ -19,6 +19,7 @@ import { HELP_CENTER_LINK, SUBMIT_TOAST_MESSAGE } from '../settings/data/constan import ConfigErrorModal from '../settings/ConfigErrorModal'; import { channelMapping, pollAsync } from '../../utils'; import HelpCenterButton from '../settings/HelpCenterButton'; +import './_FormWorkflow.scss'; export const WAITING_FOR_ASYNC_OPERATION = 'WAITING FOR ASYNC OPERATION'; @@ -107,6 +108,7 @@ const FormWorkflow = ({ closeSavedChangesModal, ] = useToggle(false); const [helpCenterLink, setHelpCenterLink] = useState(HELP_CENTER_LINK); + const [nextInProgress, setNextInProgress] = useState(false); const nextButtonConfig = step?.nextButtonConfig(formFields); const awaitingAsyncAction = stateMap && stateMap[WAITING_FOR_ASYNC_OPERATION]; @@ -131,6 +133,7 @@ const FormWorkflow = ({ } else { let advance = true; if (nextButtonConfig && nextButtonConfig.onClick) { + setNextInProgress(true); const newFormFields: FormConfigData = await nextButtonConfig.onClick({ formFields, errHandler: setFormError, @@ -140,6 +143,7 @@ const FormWorkflow = ({ if (newFormFields) { dispatch(updateFormFieldsAction({ formFields: newFormFields })); } + setNextInProgress(false); if (nextButtonConfig?.awaitSuccess) { advance = await pollAsync( () => nextButtonConfig.awaitSuccess?.awaitCondition?.({ @@ -211,6 +215,16 @@ const FormWorkflow = ({ const showBackButton = (step?.index !== undefined) && (step.index > 0) && step.showBackButton; // Show cancel button by default const showCancelButton = step?.showCancelButton === undefined || step?.showCancelButton; + let nextButtonContents = nextButtonConfig && ( + <> + {nextButtonConfig.buttonText} + {nextButtonConfig.opensNewWindow && } + + ); + if (nextInProgress) { + // show spinner if Next button operation is ongoing + nextButtonContents = ; + } return ( <> ({ {showCancelButton && } {showBackButton && } {nextButtonConfig && ( - )} diff --git a/src/components/forms/_FormWorkflow.scss b/src/components/forms/_FormWorkflow.scss new file mode 100644 index 0000000000..86e8a9828e --- /dev/null +++ b/src/components/forms/_FormWorkflow.scss @@ -0,0 +1,4 @@ +.next-button { + min-width: 60px; + min-height: 40px; +} \ No newline at end of file From df749dcd021efd6c0b54f55e50eca88fe31e3820 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Tue, 14 Nov 2023 15:19:21 -0500 Subject: [PATCH 068/124] feat: assignment modal summary after user input (#1088) --- .../cards/AssignmentModalContent.jsx | 84 ++++++++++++++----- .../cards/AssignmentModalSummary.jsx | 59 +++++++++++++ .../AssignmentModalSummaryEmptyState.jsx | 10 +++ .../AssignmentModalSummaryLearnerList.jsx | 73 ++++++++++++++++ .../cards/BaseCourseCard.jsx | 4 +- .../cards/CourseCard.test.jsx | 27 +++++- .../CreateAllocationErrorAlertModals.jsx | 6 +- .../ContentNotInCatalogErrorAlertModal.jsx | 0 .../NotEnoughBalanceAlertModal.jsx | 0 .../SystemErrorAlertModal.jsx | 0 .../cards/data/constants.js | 6 +- .../cards/data/useCourseCardMetadata.js | 4 +- .../cards/data/utils.js | 6 +- 13 files changed, 247 insertions(+), 32 deletions(-) create mode 100644 src/components/learner-credit-management/cards/AssignmentModalSummary.jsx create mode 100644 src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx create mode 100644 src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx rename src/components/learner-credit-management/cards/{status-modals => assignment-allocation-status-modals}/ContentNotInCatalogErrorAlertModal.jsx (100%) rename src/components/learner-credit-management/cards/{status-modals => assignment-allocation-status-modals}/NotEnoughBalanceAlertModal.jsx (100%) rename src/components/learner-credit-management/cards/{status-modals => assignment-allocation-status-modals}/SystemErrorAlertModal.jsx (100%) diff --git a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx index e13e317d58..b85e2ba6d5 100644 --- a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx @@ -1,5 +1,8 @@ -import React, { useState } from 'react'; +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; import PropTypes from 'prop-types'; +import debounce from 'lodash.debounce'; import { Container, Stack, @@ -12,19 +15,45 @@ import { import BaseCourseCard from './BaseCourseCard'; import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from '../data'; import { ImpactOnYourLearnerCreditBudget, ManagingThisAssignment, NextStepsForAssignedLearners } from './Collapsibles'; +import AssignmentModalSummary from './AssignmentModalSummary'; +import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from './data'; const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { - const [emailAddressesInputValue, setEmailAddressesInputValue] = useState(''); const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const [learnerEmails, setLearnerEmails] = useState([]); + const [emailAddressesInputValue, setEmailAddressesInputValue] = useState(''); + const handleEmailAddressInputChange = (e) => { const inputValue = e.target.value; - const emailAddresses = inputValue.split('\n').filter((email) => email.trim().length > 0); setEmailAddressesInputValue(inputValue); - onEmailAddressesChange(emailAddresses); }; + const handleEmailAddressesChanged = useCallback((value) => { + if (!value) { + setLearnerEmails([]); + onEmailAddressesChange([]); + } + const emails = value.split('\n').filter((email) => email.trim().length > 0); + setLearnerEmails(emails); + onEmailAddressesChange(emails); + }, [onEmailAddressesChange]); + + const debouncedHandleEmailAddressesChanged = useMemo( + () => debounce(handleEmailAddressesChanged, EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY), + [handleEmailAddressesChanged], + ); + + useEffect(() => { + debouncedHandleEmailAddressesChanged(emailAddressesInputValue); + }, [emailAddressesInputValue, debouncedHandleEmailAddressesChanged]); + + const hasLearnerEmails = learnerEmails.length > 0; + const spendAvailable = subsidyAccessPolicy.aggregates.spendAvailableUsd; + const costToAssignLearners = learnerEmails.length * course.normalizedMetadata.contentPrice; + const remainingBalanceAfterAssignment = spendAvailable - costToAssignLearners; + return ( @@ -35,7 +64,7 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { - +

    Assign to

    {

    Pay by Learner Credit

    -
    Summary
    - - -
    You haven't entered any learners yet.
    - Add learner emails to get started. -
    -
    -
    + +
    Learner Credit Budget: {subsidyAccessPolicy.displayName ?? 'Overview'}
    - - -
    Available balance
    -
    {formatPrice(subsidyAccessPolicy.aggregates.spendAvailableUsd)}
    -
    -
    + + + + + +
    Available balance
    +
    {formatPrice(spendAvailable)}
    +
    + {hasLearnerEmails && ( + +
    Total cost
    +
    -{formatPrice(costToAssignLearners)}
    +
    + )} +
    +
    +
    + {hasLearnerEmails && ( + + +
    Remaining after assignment
    +
    {formatPrice(remainingBalanceAfterAssignment)}
    +
    +
    + )} +
    diff --git a/src/components/learner-credit-management/cards/AssignmentModalSummary.jsx b/src/components/learner-credit-management/cards/AssignmentModalSummary.jsx new file mode 100644 index 0000000000..3baa98da0f --- /dev/null +++ b/src/components/learner-credit-management/cards/AssignmentModalSummary.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Card, Stack } from '@edx/paragon'; + +import { formatPrice } from '../data'; +import AssignmentModalSummaryEmptyState from './AssignmentModalSummaryEmptyState'; +import AssignmentModalSummaryLearnerList from './AssignmentModalSummaryLearnerList'; + +const AssignmentModalSummary = ({ + course, + learnerEmails, +}) => { + const learnerEmailsCount = learnerEmails.length; + const hasLearnerEmails = learnerEmailsCount > 0; + const totalAssignmentCost = learnerEmailsCount * course.normalizedMetadata.contentPrice; + + let summaryHeading = 'Summary'; + if (hasLearnerEmails) { + summaryHeading = `${summaryHeading} (${learnerEmailsCount})`; + } + return ( + <> +
    {summaryHeading}
    + + + + {hasLearnerEmails ? ( + + ) : ( + + )} + + + {hasLearnerEmails && ( + + +
    Total assignment cost
    +
    {formatPrice(totalAssignmentCost)}
    +
    +
    + )} +
    + + ); +}; + +AssignmentModalSummary.propTypes = { + course: PropTypes.shape({ + normalizedMetadata: PropTypes.shape({ + contentPrice: PropTypes.number.isRequired, + }).isRequired, + }).isRequired, + learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired, +}; + +export default AssignmentModalSummary; diff --git a/src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx b/src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx new file mode 100644 index 0000000000..694c046f23 --- /dev/null +++ b/src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +const AssignmentModalSummaryEmptyState = () => ( + <> +
    You haven't entered any learners yet.
    + Add learner emails to get started. + +); + +export default AssignmentModalSummaryEmptyState; diff --git a/src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx b/src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx new file mode 100644 index 0000000000..fdad32369a --- /dev/null +++ b/src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { v4 as uuidv4 } from 'uuid'; +import { + Button, Stack, Icon, +} from '@edx/paragon'; +import { Person } from '@edx/paragon/icons'; + +import { MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT, hasLearnerEmailsSummaryListTruncation } from './data'; + +const AssignmentModalSummaryLearnerList = ({ + course, + learnerEmails, +}) => { + const [isTruncated, setIsTruncated] = useState(hasLearnerEmailsSummaryListTruncation(learnerEmails)); + const truncatedLearnerEmails = learnerEmails.slice(0, MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT - 1); + const displayedLearnerEmails = isTruncated ? truncatedLearnerEmails : learnerEmails; + + useEffect(() => { + setIsTruncated(hasLearnerEmailsSummaryListTruncation(learnerEmails)); + }, [learnerEmails]); + + const expandCollapseMessage = isTruncated + ? `Show ${learnerEmails.length - MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT} more` + : 'Show less'; + + return ( +
      + + {displayedLearnerEmails.map((emailAddress) => ( +
    • +
      +
      + + +
      + {emailAddress} +
      +
      +
      + + {course.formattedPrice} + +
      +
    • + ))} +
      + {hasLearnerEmailsSummaryListTruncation(learnerEmails) && ( + + )} +
    + ); +}; + +AssignmentModalSummaryLearnerList.propTypes = { + course: PropTypes.shape({ + formattedPrice: PropTypes.string.isRequired, + }).isRequired, + learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired, +}; + +export default AssignmentModalSummaryLearnerList; diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx index c7a74b4339..137dd08217 100644 --- a/src/components/learner-credit-management/cards/BaseCourseCard.jsx +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -32,7 +32,7 @@ const BaseCourseCard = ({ logoAlt, title, subtitle, - price, + formattedPrice, isExecEdCourseType, courseEnrollmentInfo, execEdEnrollmentInfo, @@ -53,7 +53,7 @@ const BaseCourseCard = ({ subtitle={subtitle} actions={( -
    {price}
    +
    {formattedPrice}
    {PRICE.subText}
    )} diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/CourseCard.test.jsx index 247147b3ae..09c1bcd3c2 100644 --- a/src/components/learner-credit-management/cards/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.test.jsx @@ -21,6 +21,7 @@ import { getButtonElement, queryClient } from '../../test/testUtils'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; +import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from './data'; jest.mock('@tanstack/react-query', () => ({ ...jest.requireActual('@tanstack/react-query'), @@ -323,14 +324,12 @@ describe('Course card works as expected', () => { expect(modalCourseCard.queryByText('View course', { selector: 'a' })).not.toBeInTheDocument(); expect(getButtonElement('Assign', { screenOverride: modalCourseCard, isQueryByRole: true })).not.toBeInTheDocument(); - // Verify empty state and textarea can accept emails + // Verify empty state expect(assignmentModal.getByText('Assign to')).toBeInTheDocument(); const textareaInputLabel = assignmentModal.getByLabelText('Learner email addresses'); expect(textareaInputLabel).toBeInTheDocument(); const textareaInput = textareaInputLabel.closest('textarea'); expect(textareaInput).toBeInTheDocument(); - userEvent.type(textareaInput, 'hello@example.com{enter}world@example.com'); - expect(textareaInput).toHaveValue('hello@example.com\nworld@example.com'); expect(assignmentModal.getByText('To add more than one learner, enter one email address per line.')).toBeInTheDocument(); expect(assignmentModal.getByText('Pay by Learner Credit')).toBeInTheDocument(); expect(assignmentModal.getByText('Summary')).toBeInTheDocument(); @@ -364,6 +363,28 @@ describe('Course card works as expected', () => { expect(submitAssignmentCTA).toBeInTheDocument(); if (shouldSubmitAssignments) { + // Verify textarea receives input + userEvent.type(textareaInput, mockLearnerEmails.join('{enter}')); + expect(textareaInput).toHaveValue(mockLearnerEmails.join('\n')); + + // Verify assignment summary UI updates + await waitFor(() => { + expect(assignmentModal.getByText(`Summary (${mockLearnerEmails.length})`)).toBeInTheDocument(); + }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 }); + expect(assignmentModal.queryByText('You haven\'t entered any learners yet.')).not.toBeInTheDocument(); + expect(assignmentModal.queryByText('Add learner emails to get started.')).not.toBeInTheDocument(); + mockLearnerEmails.forEach((learnerEmail) => { + expect(assignmentModal.getByText(learnerEmail)).toBeInTheDocument(); + }); + expect(assignmentModal.getByText('Total assignment cost')).toBeInTheDocument(); + const expectedAssignmentCost = mockLearnerEmails.length * defaultProps.original.normalized_metadata.content_price; + expect(assignmentModal.getByText(formatPrice(expectedAssignmentCost))).toBeInTheDocument(); + expect(assignmentModal.getByText('Remaining after assignment')).toBeInTheDocument(); + const expectedBalanceAfterAssignment = ( + mockSubsidyAccessPolicy.aggregates.spendAvailableUsd - expectedAssignmentCost + ); + expect(assignmentModal.getByText(formatPrice(expectedBalanceAfterAssignment))).toBeInTheDocument(); + // Verify assignment is submitted successfully userEvent.click(submitAssignmentCTA); await waitFor(() => expect(mockAllocateContentAssignments).toHaveBeenCalledTimes(1)); diff --git a/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx index dc5c92ebe6..6bec89834a 100644 --- a/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx +++ b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx @@ -1,9 +1,9 @@ import React, { useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useToggle } from '@edx/paragon'; -import SystemErrorAlertModal from './status-modals/SystemErrorAlertModal'; -import ContentNotInCatalogErrorAlertModal from './status-modals/ContentNotInCatalogErrorAlertModal'; -import NotEnoughBalanceAlertModal from './status-modals/NotEnoughBalanceAlertModal'; +import SystemErrorAlertModal from './assignment-allocation-status-modals/SystemErrorAlertModal'; +import ContentNotInCatalogErrorAlertModal from './assignment-allocation-status-modals/ContentNotInCatalogErrorAlertModal'; +import NotEnoughBalanceAlertModal from './assignment-allocation-status-modals/NotEnoughBalanceAlertModal'; const CreateAllocationErrorAlertModals = ({ errorReason, diff --git a/src/components/learner-credit-management/cards/status-modals/ContentNotInCatalogErrorAlertModal.jsx b/src/components/learner-credit-management/cards/assignment-allocation-status-modals/ContentNotInCatalogErrorAlertModal.jsx similarity index 100% rename from src/components/learner-credit-management/cards/status-modals/ContentNotInCatalogErrorAlertModal.jsx rename to src/components/learner-credit-management/cards/assignment-allocation-status-modals/ContentNotInCatalogErrorAlertModal.jsx diff --git a/src/components/learner-credit-management/cards/status-modals/NotEnoughBalanceAlertModal.jsx b/src/components/learner-credit-management/cards/assignment-allocation-status-modals/NotEnoughBalanceAlertModal.jsx similarity index 100% rename from src/components/learner-credit-management/cards/status-modals/NotEnoughBalanceAlertModal.jsx rename to src/components/learner-credit-management/cards/assignment-allocation-status-modals/NotEnoughBalanceAlertModal.jsx diff --git a/src/components/learner-credit-management/cards/status-modals/SystemErrorAlertModal.jsx b/src/components/learner-credit-management/cards/assignment-allocation-status-modals/SystemErrorAlertModal.jsx similarity index 100% rename from src/components/learner-credit-management/cards/status-modals/SystemErrorAlertModal.jsx rename to src/components/learner-credit-management/cards/assignment-allocation-status-modals/SystemErrorAlertModal.jsx diff --git a/src/components/learner-credit-management/cards/data/constants.js b/src/components/learner-credit-management/cards/data/constants.js index 01af1a5313..4080ee7c98 100644 --- a/src/components/learner-credit-management/cards/data/constants.js +++ b/src/components/learner-credit-management/cards/data/constants.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import PropTypes from 'prop-types'; export const commonErrorAlertModalPropTypes = { @@ -7,3 +5,7 @@ export const commonErrorAlertModalPropTypes = { closeErrorModal: PropTypes.func.isRequired, closeAssignmentModal: PropTypes.func.isRequired, }; + +export const MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT = 15; + +export const EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY = 1000; diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js index cfb4312e20..b49ece6cf4 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.js @@ -26,7 +26,7 @@ const useCourseCardMetadata = ({ partners, title, } = course; - const price = (normalizedMetadata.contentPrice || normalizedMetadata.contentPrice === 0) ? formatPrice(normalizedMetadata.contentPrice) : 'N/A'; + const formattedPrice = (normalizedMetadata.contentPrice || normalizedMetadata.contentPrice === 0) ? formatPrice(normalizedMetadata.contentPrice) : 'N/A'; const imageSrc = cardImageUrl || cardFallbackImg; let logoSrc; @@ -57,7 +57,7 @@ const useCourseCardMetadata = ({ return { ...course, subtitle: partners.map(partner => partner.name).join(', '), - price, + formattedPrice, imageSrc, altText, logoSrc, diff --git a/src/components/learner-credit-management/cards/data/utils.js b/src/components/learner-credit-management/cards/data/utils.js index aa27d5fe37..61bb225a60 100644 --- a/src/components/learner-credit-management/cards/data/utils.js +++ b/src/components/learner-credit-management/cards/data/utils.js @@ -1,4 +1,4 @@ -/* eslint-disable import/prefer-default-export */ +import { MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT } from './constants'; export const getBudgetDisplayName = (subsidyAccessPolicy) => { let budgetDisplayName = 'budget'; @@ -7,3 +7,7 @@ export const getBudgetDisplayName = (subsidyAccessPolicy) => { } return budgetDisplayName; }; + +export const hasLearnerEmailsSummaryListTruncation = (learnerEmails) => ( + learnerEmails.length > MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT +); From ec348a69a1c963944f44816747ce2fc7917fa190 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Wed, 15 Nov 2023 14:44:41 -0500 Subject: [PATCH 069/124] feat: input validation on assignment modal (#1090) --- .../cards/AssignmentModalContent.jsx | 47 ++++-- .../cards/AssignmentModalSummary.jsx | 96 +++++++++--- .../AssignmentModalSummaryEmptyState.jsx | 4 +- .../AssignmentModalSummaryErrorState.jsx | 15 ++ .../AssignmentModalSummaryLearnerList.jsx | 3 +- .../cards/CourseCard.test.jsx | 139 +++++++++++++++--- .../cards/NewAssignmentModalButton.jsx | 17 ++- .../cards/data/utils.js | 78 ++++++++++ .../styles/index.scss | 8 + 9 files changed, 342 insertions(+), 65 deletions(-) create mode 100644 src/components/learner-credit-management/cards/AssignmentModalSummaryErrorState.jsx diff --git a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx index b85e2ba6d5..0dab3dc809 100644 --- a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx @@ -16,14 +16,18 @@ import BaseCourseCard from './BaseCourseCard'; import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from '../data'; import { ImpactOnYourLearnerCreditBudget, ManagingThisAssignment, NextStepsForAssignedLearners } from './Collapsibles'; import AssignmentModalSummary from './AssignmentModalSummary'; -import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from './data'; +import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isEmailAddressesInputValueValid } from './data'; const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const spendAvailable = subsidyAccessPolicy.aggregates.spendAvailableUsd; const [learnerEmails, setLearnerEmails] = useState([]); const [emailAddressesInputValue, setEmailAddressesInputValue] = useState(''); + const [assignmentAllocationMetadata, setAssignmentAllocationMetadata] = useState({}); + + const { contentPrice } = course.normalizedMetadata; const handleEmailAddressInputChange = (e) => { const inputValue = e.target.value; @@ -34,10 +38,10 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { if (!value) { setLearnerEmails([]); onEmailAddressesChange([]); + return; } const emails = value.split('\n').filter((email) => email.trim().length > 0); setLearnerEmails(emails); - onEmailAddressesChange(emails); }, [onEmailAddressesChange]); const debouncedHandleEmailAddressesChanged = useMemo( @@ -49,10 +53,20 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { debouncedHandleEmailAddressesChanged(emailAddressesInputValue); }, [emailAddressesInputValue, debouncedHandleEmailAddressesChanged]); - const hasLearnerEmails = learnerEmails.length > 0; - const spendAvailable = subsidyAccessPolicy.aggregates.spendAvailableUsd; - const costToAssignLearners = learnerEmails.length * course.normalizedMetadata.contentPrice; - const remainingBalanceAfterAssignment = spendAvailable - costToAssignLearners; + // Validate the learner emails emails from user input whenever it changes + useEffect(() => { + const allocationMetadata = isEmailAddressesInputValueValid({ + learnerEmails, + remainingBalance: spendAvailable, + contentPrice, + }); + setAssignmentAllocationMetadata(allocationMetadata); + if (allocationMetadata.canAllocate) { + onEmailAddressesChange(learnerEmails, { canAllocate: true }); + } else { + onEmailAddressesChange([]); + } + }, [onEmailAddressesChange, learnerEmails, contentPrice, spendAvailable]); return ( @@ -75,9 +89,15 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { rows={10} data-hj-suppress /> - - To add more than one learner, enter one email address per line. - + {assignmentAllocationMetadata.validationError ? ( + + {assignmentAllocationMetadata.validationError.message} + + ) : ( + + To add more than one learner, enter one email address per line. + + )}
    How assigning this course works
    @@ -91,6 +111,7 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => {
    @@ -104,20 +125,20 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => {
    Available balance
    {formatPrice(spendAvailable)}
    - {hasLearnerEmails && ( + {assignmentAllocationMetadata.canAllocate && (
    Total cost
    -
    -{formatPrice(costToAssignLearners)}
    +
    -{formatPrice(assignmentAllocationMetadata.totalAssignmentCost)}
    )} - {hasLearnerEmails && ( + {assignmentAllocationMetadata.canAllocate && (
    Remaining after assignment
    -
    {formatPrice(remainingBalanceAfterAssignment)}
    +
    {formatPrice(assignmentAllocationMetadata.remainingBalanceAfterAssignment)}
    )} diff --git a/src/components/learner-credit-management/cards/AssignmentModalSummary.jsx b/src/components/learner-credit-management/cards/AssignmentModalSummary.jsx index 3baa98da0f..681428bd53 100644 --- a/src/components/learner-credit-management/cards/AssignmentModalSummary.jsx +++ b/src/components/learner-credit-management/cards/AssignmentModalSummary.jsx @@ -1,18 +1,46 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Card, Stack } from '@edx/paragon'; +import classNames from 'classnames'; +import { Card, Stack, Icon } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; import { formatPrice } from '../data'; import AssignmentModalSummaryEmptyState from './AssignmentModalSummaryEmptyState'; import AssignmentModalSummaryLearnerList from './AssignmentModalSummaryLearnerList'; +import AssignmentModalSummaryErrorState from './AssignmentModalSummaryErrorState'; + +const AssignmentModalSummaryContents = ({ + hasLearnerEmails, + learnerEmails, + course, + hasInputValidationError, +}) => { + if (hasLearnerEmails) { + return ( + + ); + } + if (hasInputValidationError) { + return ; + } + return ; +}; const AssignmentModalSummary = ({ course, learnerEmails, + assignmentAllocationMetadata, }) => { - const learnerEmailsCount = learnerEmails.length; - const hasLearnerEmails = learnerEmailsCount > 0; - const totalAssignmentCost = learnerEmailsCount * course.normalizedMetadata.contentPrice; + const { + isValidInput, + learnerEmailsCount, + totalAssignmentCost, + hasEnoughBalanceForAssigment, + } = assignmentAllocationMetadata; + const hasLearnerEmails = learnerEmailsCount > 0 && isValidInput; let summaryHeading = 'Summary'; if (hasLearnerEmails) { @@ -22,23 +50,36 @@ const AssignmentModalSummary = ({ <>
    {summaryHeading}
    - - - {hasLearnerEmails ? ( - - ) : ( - - )} + + + {hasLearnerEmails && ( - - -
    Total assignment cost
    -
    {formatPrice(totalAssignmentCost)}
    + + + + {!hasEnoughBalanceForAssigment && } + +
    Total assignment cost
    +
    {formatPrice(totalAssignmentCost)}
    +
    +
    )} @@ -47,13 +88,22 @@ const AssignmentModalSummary = ({ ); }; +AssignmentModalSummaryContents.propTypes = { + hasLearnerEmails: PropTypes.bool.isRequired, + learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired, + course: PropTypes.shape().isRequired, // pass-thru prop to child component(s) + hasInputValidationError: PropTypes.bool.isRequired, +}; + AssignmentModalSummary.propTypes = { - course: PropTypes.shape({ - normalizedMetadata: PropTypes.shape({ - contentPrice: PropTypes.number.isRequired, - }).isRequired, - }).isRequired, + course: PropTypes.shape().isRequired, // pass-thru prop to child component(s) learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired, + assignmentAllocationMetadata: PropTypes.shape({ + isValidInput: PropTypes.bool, + learnerEmailsCount: PropTypes.number, + totalAssignmentCost: PropTypes.number, + hasEnoughBalanceForAssigment: PropTypes.bool, + }).isRequired, }; export default AssignmentModalSummary; diff --git a/src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx b/src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx index 694c046f23..dc8d4f1be3 100644 --- a/src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx +++ b/src/components/learner-credit-management/cards/AssignmentModalSummaryEmptyState.jsx @@ -2,8 +2,8 @@ import React from 'react'; const AssignmentModalSummaryEmptyState = () => ( <> -
    You haven't entered any learners yet.
    - Add learner emails to get started. +
    You haven't entered any learners yet.
    + Add learner emails to get started. ); diff --git a/src/components/learner-credit-management/cards/AssignmentModalSummaryErrorState.jsx b/src/components/learner-credit-management/cards/AssignmentModalSummaryErrorState.jsx new file mode 100644 index 0000000000..392dcccce3 --- /dev/null +++ b/src/components/learner-credit-management/cards/AssignmentModalSummaryErrorState.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Stack, Icon } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; + +const AssignmentModalSummaryErrorState = () => ( + + +
    +
    Learners can't be assigned as entered.
    + Please check your learner emails and try again. +
    . +
    +); + +export default AssignmentModalSummaryErrorState; diff --git a/src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx b/src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx index fdad32369a..7719678d30 100644 --- a/src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx +++ b/src/components/learner-credit-management/cards/AssignmentModalSummaryLearnerList.jsx @@ -30,13 +30,14 @@ const AssignmentModalSummaryLearnerList = ({ {displayedLearnerEmails.map((emailAddress) => (
  • -
    +
    {emailAddress}
    diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/CourseCard.test.jsx index 09c1bcd3c2..d522cb2ad6 100644 --- a/src/components/learner-credit-management/cards/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.test.jsx @@ -160,11 +160,11 @@ describe('Course card works as expected', () => { }; beforeEach(() => { + useBudgetId.mockReturnValue({ subsidyAccessPolicyId: mockSubsidyAccessPolicy.uuid }); useSubsidyAccessPolicy.mockReturnValue({ data: mockSubsidyAccessPolicy, isLoading: false, }); - useBudgetId.mockReturnValue({ subsidyAccessPolicyId: mockSubsidyAccessPolicy.uuid }); }); afterEach(() => { @@ -220,58 +220,59 @@ describe('Course card works as expected', () => { { shouldSubmitAssignments: true, hasAllocationException: true, - errorReason: 'content_not_in_catalog', + allocationExceptionReason: 'content_not_in_catalog', + shouldRetryAllocationAfterException: false, // no ability to retry after this error }, { shouldSubmitAssignments: true, hasAllocationException: true, - errorReason: 'not_enough_value_in_subsidy', - shouldRetryAfterError: false, + allocationExceptionReason: 'not_enough_value_in_subsidy', + shouldRetryAllocationAfterException: false, }, { shouldSubmitAssignments: true, hasAllocationException: true, - errorReason: 'not_enough_value_in_subsidy', - shouldRetryAfterError: true, + allocationExceptionReason: 'not_enough_value_in_subsidy', + shouldRetryAllocationAfterException: true, }, { shouldSubmitAssignments: true, hasAllocationException: true, - errorReason: 'policy_spend_limit_reached', - shouldRetryAfterError: false, + allocationExceptionReason: 'policy_spend_limit_reached', + shouldRetryAllocationAfterException: false, }, { shouldSubmitAssignments: true, hasAllocationException: true, - errorReason: 'policy_spend_limit_reached', - shouldRetryAfterError: true, + allocationExceptionReason: 'policy_spend_limit_reached', + shouldRetryAllocationAfterException: true, }, { shouldSubmitAssignments: true, hasAllocationException: true, - errorReason: null, - shouldRetryAfterError: false, + allocationExceptionReason: null, + shouldRetryAllocationAfterException: false, }, { shouldSubmitAssignments: true, hasAllocationException: true, - errorReason: null, - shouldRetryAfterError: true, + allocationExceptionReason: null, + shouldRetryAllocationAfterException: true, }, { shouldSubmitAssignments: true, hasAllocationException: false }, { shouldSubmitAssignments: false, hasAllocationException: false }, - ])('opens assignment modal, submits assignments successfully (%s)', async ({ + ])('opens assignment modal, fills out information, and submits assignments accordingly - with success or with an exception (%s)', async ({ shouldSubmitAssignments, hasAllocationException, - errorReason, - shouldRetryAfterError, + allocationExceptionReason, + shouldRetryAllocationAfterException, }) => { if (hasAllocationException) { // mock Axios error mockAllocateContentAssignments.mockRejectedValue({ customAttributes: { - httpErrorStatus: errorReason ? 422 : 500, - httpErrorResponseData: JSON.stringify([{ reason: errorReason }]), + httpErrorStatus: allocationExceptionReason ? 422 : 500, + httpErrorResponseData: JSON.stringify([{ reason: allocationExceptionReason }]), }, }); } else { @@ -295,7 +296,6 @@ describe('Course card works as expected', () => { }, }); } - useBudgetId.mockReturnValue({ subsidyAccessPolicyId: mockSubsidyAccessPolicy.uuid }); const mockInvalidateQueries = jest.fn(); useQueryClient.mockReturnValue({ invalidateQueries: mockInvalidateQueries, @@ -402,7 +402,7 @@ describe('Course card works as expected', () => { expect(getButtonElement('Try again', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'false'); // Assert the correct error modal is displayed - if (errorReason === 'content_not_in_catalog') { + if (allocationExceptionReason === 'content_not_in_catalog') { const assignmentErrorModal = getAssignmentErrorModal(); expect(assignmentErrorModal.getByText(`This course is not in your ${mockSubsidyAccessPolicy.displayName} budget's catalog`)).toBeInTheDocument(); const exitCTA = getButtonElement('Exit', { screenOverride: assignmentErrorModal }); @@ -411,11 +411,11 @@ describe('Course card works as expected', () => { // Verify all modals close (error modal + assignment modal) expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); - } else if (['not_enough_value_in_subsidy', 'policy_spend_limit_reached'].includes(errorReason)) { + } else if (['not_enough_value_in_subsidy', 'policy_spend_limit_reached'].includes(allocationExceptionReason)) { const assignmentErrorModal = getAssignmentErrorModal(); const errorModalTitle = 'Not enough balance'; expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); - if (shouldRetryAfterError) { + if (shouldRetryAllocationAfterException) { await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); } else { await simulateClickErrorModalExit(assignmentErrorModal); @@ -424,7 +424,7 @@ describe('Course card works as expected', () => { const assignmentErrorModal = getAssignmentErrorModal(); const errorModalTitle = 'Something went wrong'; expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); - if (shouldRetryAfterError) { + if (shouldRetryAllocationAfterException) { await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); } else { await simulateClickErrorModalExit(assignmentErrorModal); @@ -454,4 +454,95 @@ describe('Course card works as expected', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); } }); + + it.each([ + { + learnerEmails: ['a@a.com', 'b@bcom', 'c@c.com'], + spendAvailableUsd: 1000, + expectedValidationMessage: 'b@bcom is not a valid email.', + }, + { + learnerEmails: ['a@a.com', 'b@b.com', 'c@c.com', 'b@b.com'], + spendAvailableUsd: 1000, + expectedValidationMessage: 'b@b.com has been entered more than once.', + }, + { + learnerEmails: ['a@a.com', 'b@bcom', 'c@c.com', 'a@a.com'], + spendAvailableUsd: 1000, + expectedValidationMessage: 'b@bcom is not a valid email.', + }, + { + learnerEmails: ['a@a.com', 'b@b.com', 'c@c.com'], + spendAvailableUsd: 100, // assignment allocation will exceed available spend + expectedValidationMessage: 'The total assignment cost exceeds your available Learner Credit budget balance of $100. Please remove learners and try again.', + }, + { + learnerEmails: ['a@a.com', 'b@b.com', 'c@c.com'], + spendAvailableUsd: 1000, + expectedValidationMessage: undefined, // no validation error + }, + ])('opens assignment modal, fills out information, and handles client-side validation (%s)', async ({ + learnerEmails, + spendAvailableUsd, + expectedValidationMessage, + }) => { + useSubsidyAccessPolicy.mockReturnValue({ + data: { + ...mockSubsidyAccessPolicy, + aggregates: { + ...mockSubsidyAccessPolicy.aggregates, + spendAvailableUsd, + }, + }, + isLoading: false, + }); + + renderWithRouter(); + const assignCourseCTA = getButtonElement('Assign'); + expect(assignCourseCTA).toBeInTheDocument(); + userEvent.click(assignCourseCTA); + + const assignmentModal = within(screen.getByRole('dialog')); + + // Verify "Assign" CTA is disabled + expect(getButtonElement('Assign', { screenOverride: assignmentModal })).toBeDisabled(); + + // Verify textarea receives input + const textareaInputLabel = assignmentModal.getByLabelText('Learner email addresses'); + expect(textareaInputLabel).toBeInTheDocument(); + const textareaInput = textareaInputLabel.closest('textarea'); + expect(textareaInput).toBeInTheDocument(); + userEvent.type(textareaInput, learnerEmails.join('{enter}')); + expect(textareaInput).toHaveValue(learnerEmails.join('\n')); + + await waitFor(() => { + if (expectedValidationMessage) { + expect(assignmentModal.getByText(expectedValidationMessage)).toBeInTheDocument(); + + // Verify assigment modal summary contents handle the input validation errors, based on whether + // the validation error relates to the email addresses entered or having sufficient available spend. + const assignmentAllocationCost = learnerEmails.length * originalData.normalized_metadata.content_price; + if (assignmentAllocationCost <= spendAvailableUsd) { + const assignmentSummaryCard = assignmentModal.getByText('Learners can\'t be assigned as entered.').closest('.assignment-modal-summary-card'); + expect(assignmentSummaryCard).toBeInTheDocument(); + expect(assignmentSummaryCard).toHaveClass('invalid'); + expect(assignmentModal.getByText('Please check your learner emails and try again.')).toBeInTheDocument(); + expect(assignmentModal.queryByText('You haven\'t entered any learners yet.')).not.toBeInTheDocument(); + expect(assignmentModal.queryByText('Add learner emails to get started.')).not.toBeInTheDocument(); + expect(assignmentModal.queryByText('Total assignment cost')).not.toBeInTheDocument(); + } else { + const totalAssignmentCostCard = assignmentModal.getByText('Total assignment cost').closest('.assignment-modal-total-assignment-cost-card'); + expect(totalAssignmentCostCard).toBeInTheDocument(); + expect(totalAssignmentCostCard).toHaveClass('invalid'); + } + expect(assignmentModal.queryByText('Remaining after assignment')).not.toBeInTheDocument(); + + // Verify "Assign" CTA is still disabled + expect(getButtonElement('Assign', { screenOverride: assignmentModal })).toBeDisabled(); + } else { + // Verify "Assign" CTA is enabled + expect(getButtonElement('Assign', { screenOverride: assignmentModal })).not.toBeDisabled(); + } + }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 }); + }); }); diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index fccc05b501..2dee3ff00a 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react'; +import React, { useCallback, useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { useRouteMatch, useHistory, generatePath } from 'react-router-dom'; import { @@ -32,6 +32,7 @@ const NewAssignmentModalButton = ({ course, children }) => { const { subsidyAccessPolicyId } = useBudgetId(); const [isOpen, open, close] = useToggle(false); const [learnerEmails, setLearnerEmails] = useState([]); + const [canAllocateAssignments, setCanAllocateAssignments] = useState(false); const [assignButtonState, setAssignButtonState] = useState('default'); const [createAssignmentsErrorReason, setCreateAssignmentsErrorReason] = useState(); const { displayToastForAssignmentAllocation } = useContext(BudgetDetailPageContext); @@ -45,6 +46,17 @@ const NewAssignmentModalButton = ({ course, children }) => { setAssignButtonState('default'); }; + // Callback function for when emails are changed in the + // child AssignmentModalContent component. Must be memoized as + // the function is used within a `useEffect`'s dependency array. + const handleEmailAddressesChanged = useCallback(( + value, + { canAllocate = false } = {}, + ) => { + setLearnerEmails(value); + setCanAllocateAssignments(canAllocate); + }, []); + const handleAllocateContentAssignments = () => { const payload = snakeCaseObject({ contentPriceCents: course.normalizedMetadata.contentPrice * 100, // Convert to USD cents @@ -107,6 +119,7 @@ const NewAssignmentModalButton = ({ course, children }) => { }} variant="primary" state={assignButtonState} + disabled={!canAllocateAssignments} onClick={handleAllocateContentAssignments} /> @@ -114,7 +127,7 @@ const NewAssignmentModalButton = ({ course, children }) => { > { let budgetDisplayName = 'budget'; if (subsidyAccessPolicy.displayName) { @@ -8,6 +16,76 @@ export const getBudgetDisplayName = (subsidyAccessPolicy) => { return budgetDisplayName; }; +/** + * Determine whether the number of learner emails exceeds a certain + * threshold, whereby the list of emails should be truncated. + * @param {Array} learnerEmails List of learner emails. + * @returns True is learner emails list should be truncated; otherwise, false. + */ export const hasLearnerEmailsSummaryListTruncation = (learnerEmails) => ( learnerEmails.length > MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT ); + +/** + * Determine the validity of the learner emails user input. The input is valid if + * all emails are valid and there are no duplicates. Invalid emails and duplicate + * emails are returned. + * + * @param {Object} args Arguments. + * @param {Array} learnerEmails List of learner emails. + * @param {Number} contentPrice Price of the content (USD). + * @param {Array} remainingBalance Amount remaining in the budget (USD). + * + * @returns Object containing various properties about the validity of the learner emails + * input, including a validation error when appropriate, and whether the assignment allocation + * should proceed. + */ +export const isEmailAddressesInputValueValid = ({ + learnerEmails, + remainingBalance, + contentPrice, +}) => { + let validationError; + + const learnerEmailsCount = learnerEmails.length; + const totalAssignmentCost = contentPrice * learnerEmailsCount; + const remainingBalanceAfterAssignment = remainingBalance - totalAssignmentCost; + const hasEnoughBalanceForAssigment = remainingBalanceAfterAssignment >= 0; + + const invalidEmails = learnerEmails.filter((email) => !isEmail(email)); + const duplicateEmails = learnerEmails.filter((email, index) => learnerEmails.indexOf(email) !== index); + + const isValidInput = invalidEmails.length === 0 && duplicateEmails.length === 0; + const canAllocate = learnerEmailsCount > 0 && hasEnoughBalanceForAssigment && isValidInput; + + const ensureValidationErrorObjectExists = () => { + if (!validationError) { + validationError = {}; + } + }; + + if (!isValidInput) { + ensureValidationErrorObjectExists(); + if (invalidEmails.length > 0) { + validationError.reason = 'invalid_email'; + validationError.message = `${invalidEmails[0]} is not a valid email.`; + } else if (duplicateEmails.length > 0) { + validationError.reason = 'duplicate_email'; + validationError.message = `${duplicateEmails[0]} has been entered more than once.`; + } + } else if (!hasEnoughBalanceForAssigment) { + ensureValidationErrorObjectExists(); + validationError.reason = 'insufficient_funds'; + validationError.message = `The total assignment cost exceeds your available Learner Credit budget balance of ${formatPrice(remainingBalance)}. Please remove learners and try again.`; + } + + return { + canAllocate, + learnerEmailsCount, + isValidInput, + validationError, + totalAssignmentCost, + remainingBalanceAfterAssignment, + hasEnoughBalanceForAssigment, + }; +}; diff --git a/src/components/learner-credit-management/styles/index.scss b/src/components/learner-credit-management/styles/index.scss index 3db792b691..be434a5adb 100644 --- a/src/components/learner-credit-management/styles/index.scss +++ b/src/components/learner-credit-management/styles/index.scss @@ -29,3 +29,11 @@ .assignment-modal-collapsible-trigger { text-decoration: underline; } + +.assignment-modal-summary-card, +.assignment-modal-total-assignment-cost-card { + &.invalid { + background-color: $danger-100; + border: 1px solid $danger; + } +} From 5702148e3f0131d55c93be3b1d160ae6b9651d8a Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Tue, 14 Nov 2023 16:34:35 +0000 Subject: [PATCH 070/124] feat: network error handling for sso orchestrator self service --- .../SettingsSSOTab/NewExistingSSOConfigs.jsx | 44 +++++++-- .../SettingsSSOTab/NewSSOConfigCard.jsx | 13 ++- .../settings/SettingsSSOTab/NewSSOStepper.jsx | 33 ++++--- .../SettingsSSOTab/SSOFormWorkflowConfig.tsx | 4 +- .../settings/SettingsSSOTab/SsoErrorPage.jsx | 70 ++++++++++++++ .../settings/SettingsSSOTab/index.jsx | 71 +++++++------- .../tests/NewExistingSSOConfigs.test.jsx | 96 +++++++++++++++++++ .../tests/SettingsSSOTab.test.jsx | 19 ++++ src/components/settings/data/constants.js | 3 + src/data/images/SomethingWentWrong.svg | 19 ++++ 10 files changed, 315 insertions(+), 57 deletions(-) create mode 100644 src/components/settings/SettingsSSOTab/SsoErrorPage.jsx create mode 100644 src/data/images/SomethingWentWrong.svg diff --git a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx index 9ac1d82113..638f06f7a4 100644 --- a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx +++ b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx @@ -1,9 +1,11 @@ import _ from 'lodash'; import { + Alert, CardGrid, Skeleton, useToggle, } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; @@ -17,7 +19,7 @@ const FRESH_CONFIG_POLLING_INTERVAL = 30000; const UPDATED_CONFIG_POLLING_INTERVAL = 2000; const NewExistingSSOConfigs = ({ - configs, refreshBool, setRefreshBool, enterpriseId, + configs, refreshBool, setRefreshBool, enterpriseId, setPollingNetworkError, }) => { const [inactiveConfigs, setInactiveConfigs] = useState([]); const [activeConfigs, setActiveConfigs] = useState([]); @@ -30,6 +32,7 @@ const NewExistingSSOConfigs = ({ const [intervalMs, setIntervalMs] = React.useState(FRESH_CONFIG_POLLING_INTERVAL); const [loading, setLoading] = useState(false); const [showAlerts, openAlerts, closeAlerts] = useToggle(false); + const [updateError, setUpdateError] = useState(null); const queryClient = useQueryClient(); @@ -50,13 +53,32 @@ const NewExistingSSOConfigs = ({ }} > {configList.map((config) => ( - +
    + + {updateError?.config === config.uuid && ( +
    + (setUpdateError(null))} + > + Something went wrong behind the scenes +

    + We were unable to {updateError?.action} your SSO configuration due to an internal error. Please + {' '}try again in a couple of minutes. If the problem persists, contact enterprise customer + {' '}support. +

    +
    +
    + )} +
    ))}
    @@ -106,7 +128,10 @@ const NewExistingSSOConfigs = ({ useQuery({ queryKey: ['ssoOrchestratorConfigPoll'], queryFn: async () => { - const res = await LmsApiService.listEnterpriseSsoOrchestrationRecords(enterpriseId); + const res = await LmsApiService.listEnterpriseSsoOrchestrationRecords(enterpriseId).catch(() => { + setPollingNetworkError(true); + return { data: [] }; + }); const inProgress = res.data.filter( config => (config.submitted_at && !config.configured_at) || (config.configured_at < config.submitted_at), ); @@ -171,6 +196,7 @@ NewExistingSSOConfigs.propTypes = { refreshBool: PropTypes.bool.isRequired, setRefreshBool: PropTypes.func.isRequired, enterpriseId: PropTypes.string.isRequired, + setPollingNetworkError: PropTypes.func.isRequired, }; const mapStateToProps = state => ({ diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx index e7f94d0239..984162305b 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx @@ -14,6 +14,7 @@ const NewSSOConfigCard = ({ setLoading, setRefreshBool, refreshBool, + setUpdateError, }) => { const VALIDATED = config.validated_at; const ENABLED = config.active; @@ -32,6 +33,9 @@ const NewSSOConfigCard = ({ setLoading(true); LmsApiService.deleteEnterpriseSsoOrchestrationRecord(deletedConfig.uuid).then(() => { setRefreshBool(!refreshBool); + }).catch(() => { + setUpdateError({ config: config.uuid, action: 'delete' }); + setLoading(false); }); }; @@ -39,6 +43,9 @@ const NewSSOConfigCard = ({ setLoading(true); LmsApiService.updateEnterpriseSsoOrchestrationRecord({ active: false }, disabledConfig.uuid).then(() => { setRefreshBool(!refreshBool); + }).catch(() => { + setUpdateError({ config: config.uuid, action: 'disable' }); + setLoading(false); }); }; @@ -46,6 +53,9 @@ const NewSSOConfigCard = ({ setLoading(true); LmsApiService.updateEnterpriseSsoOrchestrationRecord({ active: true }, enabledConfig.uuid).then(() => { setRefreshBool(!refreshBool); + }).catch(() => { + setUpdateError({ config: config.uuid, action: 'enable' }); + setLoading(false); }); }; @@ -81,7 +91,7 @@ const NewSSOConfigCard = ({ {renderKeyOffIcon('existing-sso-config-card-off-not-validated-icon')} )} - {(!ENABLED || !CONFIGURED) && ( + {(!ENABLED || !CONFIGURED) && VALIDATED && ( <> {renderKeyOffIcon('existing-sso-config-card-off-icon')} @@ -213,6 +223,7 @@ NewSSOConfigCard.propTypes = { setLoading: PropTypes.func.isRequired, setRefreshBool: PropTypes.func.isRequired, refreshBool: PropTypes.bool.isRequired, + setUpdateError: PropTypes.func.isRequired, }; export default NewSSOConfigCard; diff --git a/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx b/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx index 80f23b4d69..1e928fb8d4 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOStepper.jsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import FormContextWrapper from '../../forms/FormContextWrapper'; import { SSOConfigContext } from './SSOConfigContext'; import SSOFormWorkflowConfig from './SSOFormWorkflowConfig'; +import SsoErrorPage from './SsoErrorPage'; import { camelCaseDict } from '../../../utils'; import UnsavedSSOChangesModal from './UnsavedSSOChangesModal'; import { IDP_URL_SELECTION, IDP_XML_SELECTION } from './steps/NewSSOConfigConnectStep'; @@ -16,6 +17,7 @@ const NewSSOStepper = ({ enterpriseId }) => { } = useContext(SSOConfigContext); const providerConfigCamelCase = camelCaseDict(providerConfig || {}); const [isStepperOpen, setIsStepperOpen] = useState(true); + const [configureError, setConfigureError] = useState(null); const handleCloseWorkflow = () => { setProviderConfig?.(null); setIsStepperOpen(false); @@ -27,19 +29,24 @@ const NewSSOStepper = ({ enterpriseId }) => { : IDP_XML_SELECTION; } - return (isStepperOpen - && ( -
    - -
    - ) + return ( + <> + {isStepperOpen && !configureError && ( +
    + +
    + )} + {isStepperOpen && configureError && ( + + )} + ); }; diff --git a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx index b27a8df687..2d3cd7c4a1 100644 --- a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx +++ b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx @@ -77,7 +77,7 @@ type SSOConfigFormControlVariables = { type SSOConfigFormContextData = SSOConfigCamelCase & SSOConfigFormControlVariables; -export const SSOFormWorkflowConfig = ({ enterpriseId }) => { +export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { const placeHolderButton = (buttonName?: string) => () => ({ buttonText: buttonName || 'Next', opensNewWindow: false, @@ -106,6 +106,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId }) => { updatedFormFields = updateResponse.data; } catch (error) { err = handleErrors(error); + setConfigureError(error); } } else { try { @@ -114,6 +115,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId }) => { updatedFormFields.spMetadataUrl = createResponse.data.sp_metadata_url; } catch (error) { err = handleErrors(error); + setConfigureError(error); } } if (err && errHandler) { diff --git a/src/components/settings/SettingsSSOTab/SsoErrorPage.jsx b/src/components/settings/SettingsSSOTab/SsoErrorPage.jsx new file mode 100644 index 0000000000..49450619d4 --- /dev/null +++ b/src/components/settings/SettingsSSOTab/SsoErrorPage.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + Container, + FullscreenModal, + Image, + Stack, +} from '@edx/paragon'; +import { configuration } from '../../../config'; +import { ssoStepperNetworkErrorText, ssoLPNetworkErrorText, HELP_CENTER_SAML_LINK } from '../data/constants'; +import cardImage from '../../../data/images/SomethingWentWrong.svg'; + +const SsoErrorPage = ({ + isOpen, + stepperError, +}) => { + const stepperText = stepperError ? ssoStepperNetworkErrorText : ssoLPNetworkErrorText; + return ( + { /* FullscreenModal requires an onClose prop despite hasCloseButton is false */ }} + title={( + edX logo title + )} + > + + Something went wrong +

    + We're sorry.{' '} +  Something went wrong. +

    + + +

    + {stepperText}{' '} + Please close this window and try again in a couple of minutes. If the problem persists, contact enterprise + customer support. +

    +

    + Helpful link:{' '} + Enterprise Help Center: Single Sign-On +

    +
    +
    +
    + ); +}; + +SsoErrorPage.propTypes = { + isOpen: PropTypes.bool.isRequired, + stepperError: PropTypes.bool, +}; + +SsoErrorPage.defaultProps = { + stepperError: false, +}; + +export default SsoErrorPage; diff --git a/src/components/settings/SettingsSSOTab/index.jsx b/src/components/settings/SettingsSSOTab/index.jsx index a1f5599415..14e0175e74 100644 --- a/src/components/settings/SettingsSSOTab/index.jsx +++ b/src/components/settings/SettingsSSOTab/index.jsx @@ -7,6 +7,7 @@ import { Add, WarningFilled } from '@edx/paragon/icons'; import { HELP_CENTER_SAML_LINK } from '../data/constants'; import { useExistingSSOConfigs, useExistingProviderData } from './hooks'; import NoSSOCard from './NoSSOCard'; +import SsoErrorPage from './SsoErrorPage'; import ExistingSSOConfigs from './ExistingSSOConfigs'; import NewExistingSSOConfigs from './NewExistingSSOConfigs'; import NewSSOConfigForm from './NewSSOConfigForm'; @@ -27,6 +28,7 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { const [showNoSSOCard, setShowNoSSOCard] = useState(false); const { AUTH0_SELF_SERVICE_INTEGRATION } = features; const [isOpen, open, close] = useToggle(false); + const [pollingNetworkError, setPollingNetworkError] = useState(false); const newConfigurationButtonOnClick = async () => { Promise.all(existingConfigs.map(config => LmsApiService.updateEnterpriseSsoOrchestrationRecord( @@ -115,41 +117,44 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => {
    {(!isLoading || !pdIsLoading) && ( -
    - {/* providerConfig represents the currently selected config to edit/create, if there are - existing configs but no providerConfig then we can safely render the listings page */} - {existingConfigs?.length > 0 && (providerConfig === null) && ( - + <> + {!error && ( +
    + {/* providerConfig represents the currently selected config to edit/create, if there are + existing configs but no providerConfig then we can safely render the listings page */} + {existingConfigs?.length > 0 && (providerConfig === null) && ( + + )} + {/* Nothing found so guide user to creation/edit form */} + {showNoSSOCard && ( + + )} + {/* Since we found a selected providerConfig we know we are in editing mode and can safely + render the create/edit form */} + {((existingConfigs?.length > 0 && providerConfig !== null) || showNewSSOForm) && ()} + {pdError && ( + + An error occurred loading the SAML data:

    {pdError?.message}

    +
    + )} + setInfoMessage(null)} + show={infoMessage?.length > 0} + > + {infoMessage} + +
    )} - {/* Nothing found so guide user to creation/edit form */} - {showNoSSOCard && } - {/* Since we found a selected providerConfig we know we are in editing mode and can safely - render the create/edit form */} - {((existingConfigs?.length > 0 && providerConfig !== null) || showNewSSOForm) && ()} - {error && ( - - An error occurred loading the SAML configs:

    {error?.message}

    -
    + {(error || pollingNetworkError) && ( + )} - {pdError && ( - - An error occurred loading the SAML data:

    {pdError?.message}

    -
    - )} - {infoMessage && ( - setInfoMessage(null)} - show={infoMessage.length > 0} - > - {infoMessage} - - )} -
    + )} {(isLoading || pdIsLoading) && }
  • diff --git a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx index 5ac24d2601..2a0b1950ed 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx @@ -140,6 +140,8 @@ const contextValue = { setRefreshBool: jest.fn(), }; +const mockSetPollingNetworkError = jest.fn(); + const setupNewExistingSSOConfigs = (configs) => { features.AUTH0_SELF_SERVICE_INTEGRATION = true; return render( @@ -152,6 +154,7 @@ const setupNewExistingSSOConfigs = (configs) => { configs={configs} refreshBool={false} setRefreshBool={mockSetRefreshBool} + setPollingNetworkError={mockSetPollingNetworkError} /> @@ -166,6 +169,11 @@ describe('New Existing SSO Configs tests', () => { jest.clearAllMocks(); }); test('checks and sets in progress configs', async () => { + LmsApiService.listEnterpriseSsoOrchestrationRecords.mockImplementation(() => Promise.resolve({ + data: [ + { submitted_at: '2022-04-12T19:51:25Z', configured_at: undefined }, + ], + })); setupNewExistingSSOConfigs(inProgressConfig); expect( screen.queryByText( @@ -174,6 +182,11 @@ describe('New Existing SSO Configs tests', () => { ).toBeInTheDocument(); }); test('checks and sets not configured configs', async () => { + LmsApiService.listEnterpriseSsoOrchestrationRecords.mockImplementation(() => Promise.resolve({ + data: [ + { submitted_at: '2022-04-12T19:51:25Z', configured_at: undefined }, + ], + })); setupNewExistingSSOConfigs(notConfiguredConfig); expect( screen.queryByText( @@ -220,6 +233,9 @@ describe('New Existing SSO Configs tests', () => { expect(mockSetRefreshBool).toHaveBeenCalledTimes(2); }); test('enabling config sets loading and renders skeleton', async () => { + LmsApiService.listEnterpriseSsoOrchestrationRecords.mockImplementation(() => Promise.resolve({ + data: [{}], + })); const spy = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord'); spy.mockImplementation(() => Promise.resolve({})); setupNewExistingSSOConfigs(inactiveConfig); @@ -234,4 +250,84 @@ describe('New Existing SSO Configs tests', () => { ), ).toBeInTheDocument()); }); + test('config card enable action network error alert', async () => { + LmsApiService.listEnterpriseSsoOrchestrationRecords.mockImplementation(() => Promise.resolve({ + data: [{}], + })); + const spy = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord'); + spy.mockRejectedValue({}); + + setupNewExistingSSOConfigs(inactiveConfig); + const button = screen.getByTestId('existing-sso-config-card-enable-button'); + act(() => { + userEvent.click(button); + }); + expect(spy).toBeCalledTimes(1); + await waitFor(() => expect( + screen.queryByText( + 'Something went wrong behind the scenes', + ), + ).toBeInTheDocument()); + + const dismissAlertButton = screen.getByText('Dismiss'); + act(() => { + userEvent.click(dismissAlertButton); + }); + expect( + screen.queryByText( + 'Something went wrong behind the scenes', + ), + ).not.toBeInTheDocument(); + }); + test('config card disable action network error alert', async () => { + LmsApiService.listEnterpriseSsoOrchestrationRecords.mockImplementation(() => Promise.resolve({ + data: [{}], + })); + const spy = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord'); + spy.mockRejectedValue({}); + + setupNewExistingSSOConfigs(activeConfig); + const kebobButton = screen.getByTestId('existing-sso-config-card-dropdown'); + act(() => { + userEvent.click(kebobButton); + }); + const button = screen.getByTestId('existing-sso-config-disable-dropdown'); + act(() => { + userEvent.click(button); + }); + await waitFor(() => expect( + screen.queryByText( + 'Something went wrong behind the scenes', + ), + ).toBeInTheDocument()); + }); + test('config card delete action network error alert', async () => { + LmsApiService.listEnterpriseSsoOrchestrationRecords.mockImplementation(() => Promise.resolve({ + data: [{}], + })); + const spy = jest.spyOn(LmsApiService, 'deleteEnterpriseSsoOrchestrationRecord'); + spy.mockRejectedValue({}); + + setupNewExistingSSOConfigs(inactiveConfig); + const kebobButton = screen.getByTestId('existing-sso-config-card-dropdown'); + act(() => { + userEvent.click(kebobButton); + }); + const button = screen.getByTestId('existing-sso-config-delete-dropdown'); + act(() => { + userEvent.click(button); + }); + await waitFor(() => expect( + screen.queryByText( + 'Something went wrong behind the scenes', + ), + ).toBeInTheDocument()); + }); + test('polling network error sets network error state', async () => { + const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords'); + spy.mockRejectedValue({}); + setupNewExistingSSOConfigs(inProgressConfig); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + expect(mockSetPollingNetworkError).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx b/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx index 852d2c292a..41d6df6d8b 100644 --- a/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/SettingsSSOTab.test.jsx @@ -106,4 +106,23 @@ describe('SAML Config Tab', () => { 'Great news! Your test was successful and your new SSO integration is live and ready to use.', )).toBeInTheDocument(); }); + test('network errors trigger sso error page', async () => { + features.AUTH0_SELF_SERVICE_INTEGRATION = true; + const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords'); + spy.mockRejectedValue({}); + await waitFor(() => render( + + + + + , + + , + )); + await waitFor(() => expect( + screen.getByTestId( + 'sso-network-error-image', + ), + ).toBeInTheDocument()); + }); }); diff --git a/src/components/settings/data/constants.js b/src/components/settings/data/constants.js index 117636d3df..23915bb12b 100644 --- a/src/components/settings/data/constants.js +++ b/src/components/settings/data/constants.js @@ -57,6 +57,9 @@ export const INVALID_ODATA_API_TIMEOUT_INTERVAL = 'OData API timeout interval mu export const MAX_UNIVERSAL_LINKS = 100; +export const ssoStepperNetworkErrorText = 'We were unable to configure your SSO due to an internal error.'; +export const ssoLPNetworkErrorText = 'We were unable to load your SSO details due to an internal error.'; + /** * Used as tab values and in router params */ diff --git a/src/data/images/SomethingWentWrong.svg b/src/data/images/SomethingWentWrong.svg new file mode 100644 index 0000000000..751c4cd9cd --- /dev/null +++ b/src/data/images/SomethingWentWrong.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 5cc1b8d0eeafb43530fa4cfeaaacab7392339a1a Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 17 Nov 2023 09:05:20 -0500 Subject: [PATCH 071/124] feat: display refunds in spent table when backed by transactions API (#1091) --- .../AssignmentDetailsTableCell.jsx | 6 +-- .../LearnerCreditAllocationTable.jsx | 7 ++- .../SpendTableAmountContents.jsx | 32 +++++++++++ .../SpendTableEnrollmentDetails.jsx | 19 +++++-- .../useBudgetDetailActivityOverview.test.jsx | 39 ++++++++++---- .../data/hooks/useOfferRedemptions.js | 13 +++-- .../data/hooks/useOfferRedemptions.test.jsx | 20 ++++--- .../learner-credit-management/data/utils.js | 54 +++++++++++++------ .../tests/BudgetDetailPage.test.jsx | 49 +++++++++++++++-- 9 files changed, 180 insertions(+), 59 deletions(-) create mode 100644 src/components/learner-credit-management/SpendTableAmountContents.jsx diff --git a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx index b04f3b0a9b..00b20f2419 100644 --- a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Hyperlink } from '@edx/paragon'; +import { Stack, Hyperlink } from '@edx/paragon'; import { configuration } from '../../config'; import EmailAddressTableCell from './EmailAddressTableCell'; @@ -9,7 +9,7 @@ import EmailAddressTableCell from './EmailAddressTableCell'; const AssignmentDetailsTableCell = ({ row, enterpriseSlug }) => { const { ENTERPRISE_LEARNER_PORTAL_URL } = configuration; return ( - <> + { {row.original.contentTitle || 'View Course'}
    - + ); }; diff --git a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx index 8b412b066a..c5a471326d 100644 --- a/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx +++ b/src/components/learner-credit-management/LearnerCreditAllocationTable.jsx @@ -9,8 +9,8 @@ import { PAGE_SIZE, DEFAULT_PAGE, formatDate, - formatPrice, } from './data'; +import SpendTableAmountContents from './SpendTableAmountContents'; const FilterStatus = (rest) => ; @@ -45,7 +45,7 @@ const LearnerCreditAllocationTable = ({ { Header: 'Amount', accessor: 'courseListPrice', - Cell: ({ row }) => formatPrice(row.values.courseListPrice), + Cell: SpendTableAmountContents, disableFilters: true, }, ]} @@ -73,10 +73,9 @@ LearnerCreditAllocationTable.propTypes = { tableData: PropTypes.shape({ results: PropTypes.arrayOf(PropTypes.shape({ userEmail: PropTypes.string, - courseTitle: PropTypes.string.isRequired, + courseTitle: PropTypes.string, courseListPrice: PropTypes.number.isRequired, enrollmentDate: PropTypes.string.isRequired, - courseProductLine: PropTypes.string.isRequired, })), itemCount: PropTypes.number.isRequired, pageCount: PropTypes.number.isRequired, diff --git a/src/components/learner-credit-management/SpendTableAmountContents.jsx b/src/components/learner-credit-management/SpendTableAmountContents.jsx new file mode 100644 index 0000000000..2764439f63 --- /dev/null +++ b/src/components/learner-credit-management/SpendTableAmountContents.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Stack } from '@edx/paragon'; + +import { formatPrice } from './data'; + +const SpendTableAmountContents = ({ row }) => { + const formattedContentPrice = formatPrice(row.original.courseListPrice); + return ( + + {row.original.reversal && ( +
    +{formattedContentPrice}
    + )} +
    -{formattedContentPrice}
    +
    + ); +}; + +const rowPropType = PropTypes.shape({ + original: PropTypes.shape({ + courseListPrice: PropTypes.number.isRequired, + reversal: PropTypes.shape({ + created: PropTypes.string, + }), + }).isRequired, +}).isRequired; + +SpendTableAmountContents.propTypes = { + row: rowPropType, +}; + +export default SpendTableAmountContents; diff --git a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx index a0d61f8dda..8ec52fd7cb 100644 --- a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx +++ b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx @@ -1,17 +1,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Hyperlink } from '@edx/paragon'; +import { Stack, Hyperlink } from '@edx/paragon'; import { getConfig } from '@edx/frontend-platform'; import EmailAddressTableCell from './EmailAddressTableCell'; +import { formatDate } from './data'; const SpendTableEnrollmentDetailsContents = ({ row, enableLearnerPortal, enterpriseSlug, }) => ( - <> + + {row.original.reversal && ( +
    + Refunded on {formatDate(row.original.reversal.created)} +
    + )} - {row.original.courseTitle} + {row.original.courseTitle || 'View course'} ) : ( {row.original.courseTitle} )}
    - + ); const rowPropType = PropTypes.shape({ original: PropTypes.shape({ courseKey: PropTypes.string.isRequired, - courseTitle: PropTypes.string.isRequired, + courseTitle: PropTypes.string, userEmail: PropTypes.string, enterpriseEnrollmentId: PropTypes.number, fulfillmentIdentifier: PropTypes.string, + reversal: PropTypes.shape({ + created: PropTypes.string, + }), }).isRequired, }).isRequired; diff --git a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx index 3290ef90dc..351f8b5cf4 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx @@ -13,6 +13,7 @@ import { mockSubsidyAccessPolicyUUID, } from '../tests/constants'; import { queryClient } from '../../../test/testUtils'; +import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; jest.mock('./useBudgetId'); jest.mock('./useSubsidyAccessPolicy'); @@ -102,26 +103,44 @@ describe('useBudgetDetailActivityOverview', () => { }, }); } + const mockFetchCustomerTransactions = jest.spyOn(SubsidyApiService, 'fetchCustomerTransactions'); const mockFetchCourseEnrollments = jest.spyOn(EnterpriseDataApiService, 'fetchCourseEnrollments'); - mockFetchCourseEnrollments.mockResolvedValue({ - data: { - count: 1, - results: [{ id: 'mock-course-enrollment-id' }], - }, - }); + const mockSubsidyTransaction = { uuid: 'mock-transaction-uuid' }; + const mockAnalyticsApiRedemption = { id: 'mock-course-enrollment-id' }; + + if (isTopDownAssignmentEnabled) { + mockFetchCustomerTransactions.mockResolvedValue({ + data: { + count: 1, + results: [mockSubsidyTransaction], + }, + }); + } else { + mockFetchCourseEnrollments.mockResolvedValue({ + data: { + count: 1, + results: [mockAnalyticsApiRedemption], + }, + }); + } const { result, waitForNextUpdate } = renderHook( () => useBudgetDetailActivityOverview({ enterpriseUUID: mockEnterpriseUUID, - isTopDownAssignmentEnabled: true, + isTopDownAssignmentEnabled, }), { wrapper }, ); expect(useSubsidyAccessPolicy).toHaveBeenCalledWith(mockSubsidyAccessPolicyUUID); - expect(mockFetchCourseEnrollments).toHaveBeenCalledTimes(1); - if (hasAssignableBudget) { + if (isTopDownAssignmentEnabled) { + expect(mockFetchCustomerTransactions).toHaveBeenCalledTimes(1); + } else { + expect(mockFetchCourseEnrollments).toHaveBeenCalledTimes(1); + } + + if (isTopDownAssignmentEnabled && hasAssignableBudget) { expect(mockListContentAssignments).toHaveBeenCalledTimes(1); } else { expect(mockListContentAssignments).not.toHaveBeenCalled(); @@ -132,7 +151,7 @@ describe('useBudgetDetailActivityOverview', () => { const expectedData = { spentTransactions: { count: 1, - results: [{ id: 'mock-course-enrollment-id' }], + results: [isTopDownAssignmentEnabled ? mockSubsidyTransaction : mockAnalyticsApiRedemption], }, }; diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js index c5f824b8a1..2898577a0a 100644 --- a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js +++ b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js @@ -49,7 +49,7 @@ const useOfferRedemptions = ( enterpriseUUID, offerId = null, budgetId = null, - shouldFetchSubsidyTransactions = false, + isTopDownAssignmentEnabled = false, ) => { const shouldTrackFetchEvents = useRef(false); const [isLoading, setIsLoading] = useState(true); @@ -63,14 +63,17 @@ const useOfferRedemptions = ( const fetchOfferRedemptions = useCallback((args) => { const fetch = async () => { try { + const shouldFetchSubsidyTransactions = budgetId && isTopDownAssignmentEnabled; setIsLoading(true); const options = { page: args.pageIndex + 1, // `DataTable` uses zero-indexed array pageSize: args.pageSize, - ignoreNullCourseListPrice: true, }; + if (!shouldFetchSubsidyTransactions) { + options.ignoreNullCourseListPrice = true; + } if (budgetId !== null) { - options.budgetId = budgetId; + options[shouldFetchSubsidyTransactions ? 'subsidyAccessPolicyUuid' : 'budgetId'] = budgetId; } if (offerId !== null) { options.offerId = offerId; @@ -83,7 +86,7 @@ const useOfferRedemptions = ( } let data; let transformedTableResults; - if (budgetId && shouldFetchSubsidyTransactions) { + if (shouldFetchSubsidyTransactions) { const response = await SubsidyApiService.fetchCustomerTransactions( subsidyAccessPolicy?.subsidyUuid, options, @@ -131,7 +134,7 @@ const useOfferRedemptions = ( offerId, budgetId, shouldTrackFetchEvents, - shouldFetchSubsidyTransactions, + isTopDownAssignmentEnabled, subsidyAccessPolicy?.subsidyUuid, ]); diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx index 9b5d1c3c9f..0738b5bf1a 100644 --- a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx @@ -66,29 +66,29 @@ describe('useOfferRedemptions', () => { { budgetId: 'test-budget-id', offerId: undefined, - shouldFetchSubsidyTransactions: true, + isTopDownAssignmentEnabled: true, }, { budgetId: 'test-budget-id', offerId: undefined, - shouldFetchSubsidyTransactions: false, + isTopDownAssignmentEnabled: false, }, { budgetId: undefined, offerId: mockEnterpriseOffer.id, - shouldFetchSubsidyTransactions: false, + isTopDownAssignmentEnabled: false, }, - ])('should fetch enrollment/redemptions metadata for enterprise offer', async ({ + ])('should fetch enrollment/redemptions metadata for budget (%s)', async ({ budgetId, offerId, - shouldFetchSubsidyTransactions, + isTopDownAssignmentEnabled, }) => { EnterpriseDataApiService.fetchCourseEnrollments.mockResolvedValueOnce({ data: mockOfferEnrollmentsResponse }); SubsidyApiService.fetchCustomerTransactions.mockResolvedValueOnce({ data: mockSubsidyTransactionResponse }); useSubsidyAccessPolicy.mockReturnValue({ data: { subsidyUuid } }); const { result, waitForNextUpdate } = renderHook( - () => useOfferRedemptions(TEST_ENTERPRISE_UUID, offerId, budgetId, shouldFetchSubsidyTransactions), + () => useOfferRedemptions(TEST_ENTERPRISE_UUID, offerId, budgetId, isTopDownAssignmentEnabled), { wrapper }, ); @@ -116,15 +116,13 @@ describe('useOfferRedemptions', () => { await waitForNextUpdate(); - if (budgetId && shouldFetchSubsidyTransactions) { + if (budgetId && isTopDownAssignmentEnabled) { const expectedApiOptions = { page: 1, pageSize: 20, - offerId, ordering: '-enrollment_date', // default sort order search: mockOfferEnrollments[0].user_email, - ignoreNullCourseListPrice: true, - budgetId, + subsidyAccessPolicyUuid: budgetId, }; expect(SubsidyApiService.fetchCustomerTransactions).toHaveBeenCalledWith( subsidyUuid, @@ -146,7 +144,7 @@ describe('useOfferRedemptions', () => { ); } - const mockExpectedResultsObj = shouldFetchSubsidyTransactions ? [{ + const mockExpectedResultsObj = isTopDownAssignmentEnabled ? [{ courseListPrice: 10, courseTitle, userEmail, diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 5780e28eb2..a055fd567c 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -1,6 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import dayjs from 'dayjs'; -import { camelCaseObject } from '@edx/frontend-platform'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; +import { logInfo } from '@edx/frontend-platform/logging'; import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, @@ -10,6 +11,7 @@ import { import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiService'; +import SubsidyApiService from '../../../data/services/EnterpriseSubsidyApiService'; /** * Transforms offer summary from API for display in the UI, guarding @@ -90,14 +92,15 @@ export const transformUtilizationTableResults = results => results.map(result => courseTitle: result.courseTitle, courseListPrice: result.courseListPrice, enrollmentDate: result.enrollmentDate, - courseProductLine: result.courseProductLine, uuid: uuidv4(), courseKey: result.courseKey, })); export const transformUtilizationTableSubsidyTransactionResults = results => results.map(result => ({ created: result.created, - enterpriseEnrollmentId: result.fulfillmentIdentifier, + enrollmentDate: result.created, + fulfillmentIdentifier: result.fulfillmentIdentifier, + reversal: result.reversal, userEmail: result.lmsUserEmail, courseTitle: result.contentTitle, courseListPrice: result.unit === 'usd_cents' ? -1 * (result.quantity / 100) : -1 * results.quantity, @@ -249,25 +252,43 @@ export async function fetchContentAssignments(assignmentConfigurationUUID, optio */ export async function fetchSpentTransactions({ enterpriseUUID, - subsidyAccessPolicyId, - enterpriseOfferId, + subsidyAccessPolicy, + budgetId, + isTopDownAssignmentEnabled, }) { const options = { page: 1, pageSize: 25, - ignoreNullCourseListPrice: true, }; - if (subsidyAccessPolicyId) { - options.budgetId = subsidyAccessPolicyId; - } else if (enterpriseOfferId) { - options.offerId = enterpriseOfferId; + let response; + const shouldFetchSubsidyTransactions = !!subsidyAccessPolicy && isTopDownAssignmentEnabled; + if (shouldFetchSubsidyTransactions) { + options.subsidyAccessPolicyUuid = budgetId; + // Feature flag is enabled and the budget is a subsidy access policy, so pull from + // the `transactions` API via enterprise-subsidy. + response = await SubsidyApiService.fetchCustomerTransactions( + subsidyAccessPolicy.subsidyUuid, + options, + ); + } else { + // Feature flag disabled or budget is not a subsidy access policy; continue to call analytics API. + if (subsidyAccessPolicy) { + options.budgetId = budgetId; + } else { + options.offerId = budgetId; + } + options.ignoreNullCourseListPrice = true; + response = await EnterpriseDataApiService.fetchCourseEnrollments( + enterpriseUUID, + options, + ); + } + + if (!response) { + logInfo('[fetchSpentTransactions] Spent transactions were not fetched from API. No budget identifier provided.'); } - const response = await EnterpriseDataApiService.fetchCourseEnrollments( - enterpriseUUID, - options, - ); return camelCaseObject(response.data); } @@ -295,8 +316,9 @@ export async function retrieveBudgetDetailActivityOverview({ const promisesToFulfill = [ fetchSpentTransactions({ enterpriseUUID, - subsidyAccessPolicyId: subsidyAccessPolicy?.uuid, - enterpriseOfferId: budgetId, + subsidyAccessPolicy, + budgetId, + isTopDownAssignmentEnabled, }), ]; if (isBudgetAssignable) { diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index d8729c61b5..62f31fd0b4 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -21,6 +21,7 @@ import { formatDate, DEFAULT_PAGE, PAGE_SIZE, + formatPrice, } from '../data'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; import { @@ -61,6 +62,7 @@ const initialStoreState = { }; const mockLearnerEmail = 'edx@example.com'; +const mockSecondLearnerEmail = 'edx001@example.com'; const mockCourseKey = 'edX+DemoX'; const mockContentTitle = 'edx Demo'; @@ -106,6 +108,26 @@ const mockLearnerContentAssignment = { actions: [mockSuccessfulLinkedLearnerAction, mockSuccessfulNotifiedAction], errorReason: null, }; +const mockEnrollmentTransactionReversal = { + uuid: 'test-transaction-reversal-uuid', + created: '2023-10-31', +}; +const mockEnrollmentTransaction = { + uuid: 'test-transaction-uuid', + enrollmentDate: '2023-10-28', + courseKey: mockCourseKey, + courseTitle: mockContentTitle, + userEmail: mockLearnerEmail, + fulfillmentIdentifier: 'test-fulfillment-identifier', + courseListPrice: 100, + reversal: null, +}; +const mockEnrollmentTransactionWithReversal = { + ...mockEnrollmentTransaction, + uuid: 'test-transaction-with-reversal-uuid', + userEmail: mockSecondLearnerEmail, + reversal: mockEnrollmentTransactionReversal, +}; const defaultEnterpriseSubsidiesContextValue = { isLoading: false, @@ -230,8 +252,8 @@ describe('', () => { useBudgetDetailActivityOverview.mockReturnValue({ isLoading: false, data: { - contentAssignments: undefined, - spentTransactions: { count: 1 }, + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, }, }); useBudgetContentAssignments.mockReturnValue({ @@ -263,7 +285,7 @@ describe('', () => { expect(spentSection.getByText('No results found')).toBeInTheDocument(); }); - it('renders with assigned table empty state with spent table and catalog tab available for assignable budgets', () => { + it('renders with assigned table empty state with spent table and catalog tab available for assignable budgets', async () => { useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', @@ -276,7 +298,7 @@ describe('', () => { isLoading: false, data: { contentAssignments: { count: 0 }, - spentTransactions: { count: 1 }, + spentTransactions: { count: 2 }, }, }); useBudgetContentAssignments.mockReturnValue({ @@ -290,7 +312,11 @@ describe('', () => { }); useOfferRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, + offerRedemptions: { + itemCount: 2, + pageCount: 1, + results: [mockEnrollmentTransaction, mockEnrollmentTransactionWithReversal], + }, fetchOfferRedemptions: jest.fn(), }); renderWithRouter(); @@ -302,6 +328,19 @@ describe('', () => { // Catalog tab exists and is NOT active expect(screen.getByText('Catalog').getAttribute('aria-selected')).toBe('false'); + + // Spend table renders rows of data + const spentSection = within(screen.getByText('Spent').closest('section')); + expect(spentSection.queryByText('No results found')).not.toBeInTheDocument(); + expect(spentSection.getByText(mockLearnerEmail)).toBeInTheDocument(); + expect(spentSection.getByText(mockSecondLearnerEmail)).toBeInTheDocument(); + expect(spentSection.queryAllByText(mockContentTitle, { selector: 'a' })).toHaveLength(2); + expect(spentSection.queryAllByText(`-${formatPrice(mockEnrollmentTransaction.courseListPrice)}`)).toHaveLength(2); + + // Includes reversal messaging on table row, when appropriate + const transactionRowWithReversal = within(spentSection.getByText(mockSecondLearnerEmail).closest('tr')); + expect(transactionRowWithReversal.getByText(`Refunded on ${formatDate(mockEnrollmentTransactionReversal.created)}`)).toBeInTheDocument(); + expect(transactionRowWithReversal.getByText(`+${formatPrice(mockEnrollmentTransaction.courseListPrice)}`)).toBeInTheDocument(); }); it('renders with assigned table data and handles table refresh', () => { From bd8455be163df4946f5c26d1ed094dcd744a3991 Mon Sep 17 00:00:00 2001 From: Alex Dusenbery Date: Mon, 20 Nov 2023 11:19:55 -0500 Subject: [PATCH 072/124] fix: list transactions from the admin-tx view ENT-7999 --- src/data/services/EnterpriseSubsidyApiService.js | 2 +- src/data/services/tests/EnterpriseSubsidyApiService.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/services/EnterpriseSubsidyApiService.js b/src/data/services/EnterpriseSubsidyApiService.js index 84550170fa..1b631a86fe 100644 --- a/src/data/services/EnterpriseSubsidyApiService.js +++ b/src/data/services/EnterpriseSubsidyApiService.js @@ -16,7 +16,7 @@ class SubsidyApiService { const queryParams = new URLSearchParams({ ...snakeCaseObject(options), }); - const url = `${SubsidyApiService.baseUrlV2}/subsidies/${subsidyUuid}/transactions/?${queryParams.toString()}`; + const url = `${SubsidyApiService.baseUrlV2}/subsidies/${subsidyUuid}/admin/transactions/?${queryParams.toString()}`; return SubsidyApiService.apiClient().get(url); } diff --git a/src/data/services/tests/EnterpriseSubsidyApiService.test.js b/src/data/services/tests/EnterpriseSubsidyApiService.test.js index 6c894bc1f8..a8f04eeae9 100644 --- a/src/data/services/tests/EnterpriseSubsidyApiService.test.js +++ b/src/data/services/tests/EnterpriseSubsidyApiService.test.js @@ -17,7 +17,7 @@ describe('EnterpriseSubsidyApiService', () => { }); test('fetchCustomerTransactions calls the API to fetch transactions by enterprise subsidy', () => { const mockSubsidyUUID = 'test-subsidy-uuid'; - const expectedUrl = `${SubsidyApiService.baseUrlV2}/subsidies/${mockSubsidyUUID}/transactions/?`; + const expectedUrl = `${SubsidyApiService.baseUrlV2}/subsidies/${mockSubsidyUUID}/admin/transactions/?`; SubsidyApiService.fetchCustomerTransactions(mockSubsidyUUID); expect(axios.get).toBeCalledWith(expectedUrl); }); From 461ccc770f39a3fec3d5dff2cfee15c27eb734c5 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Mon, 20 Nov 2023 15:41:41 -0500 Subject: [PATCH 073/124] fix: only request committed transactions for spent table (#1095) --- src/data/services/EnterpriseSubsidyApiService.js | 1 + src/data/services/tests/EnterpriseSubsidyApiService.test.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/data/services/EnterpriseSubsidyApiService.js b/src/data/services/EnterpriseSubsidyApiService.js index 1b631a86fe..316b3d471c 100644 --- a/src/data/services/EnterpriseSubsidyApiService.js +++ b/src/data/services/EnterpriseSubsidyApiService.js @@ -14,6 +14,7 @@ class SubsidyApiService { static fetchCustomerTransactions(subsidyUuid, options = {}) { const queryParams = new URLSearchParams({ + state: 'committed', ...snakeCaseObject(options), }); const url = `${SubsidyApiService.baseUrlV2}/subsidies/${subsidyUuid}/admin/transactions/?${queryParams.toString()}`; diff --git a/src/data/services/tests/EnterpriseSubsidyApiService.test.js b/src/data/services/tests/EnterpriseSubsidyApiService.test.js index a8f04eeae9..09a67c8e1b 100644 --- a/src/data/services/tests/EnterpriseSubsidyApiService.test.js +++ b/src/data/services/tests/EnterpriseSubsidyApiService.test.js @@ -15,12 +15,14 @@ describe('EnterpriseSubsidyApiService', () => { beforeEach(() => { jest.clearAllMocks(); }); + test('fetchCustomerTransactions calls the API to fetch transactions by enterprise subsidy', () => { const mockSubsidyUUID = 'test-subsidy-uuid'; - const expectedUrl = `${SubsidyApiService.baseUrlV2}/subsidies/${mockSubsidyUUID}/admin/transactions/?`; + const expectedUrl = `${SubsidyApiService.baseUrlV2}/subsidies/${mockSubsidyUUID}/admin/transactions/?state=committed`; SubsidyApiService.fetchCustomerTransactions(mockSubsidyUUID); expect(axios.get).toBeCalledWith(expectedUrl); }); + test('getSubsidyByCustomerUUID calls the API to fetch subsides by enterprise customer UUID', () => { const mockCustomerUUID = 'test-customer-uuid'; const expectedUrl = `${SubsidyApiService.baseUrlV1}/subsidies/?enterprise_customer_uuid=${mockCustomerUUID}`; From c0e0bc11f5704563b8025965e3d98794637ba898 Mon Sep 17 00:00:00 2001 From: Alex Dusenbery Date: Mon, 20 Nov 2023 16:52:57 -0500 Subject: [PATCH 074/124] feat: query for learner_state__in related to API change https://github.com/openedx/enterprise-access/pull/334 ENT-7809 --- .../services/EnterpriseAccessApiService.js | 13 +++++++++---- .../tests/EnterpriseAccessApiService.test.js | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index b115f194fb..d0035b3e63 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -147,15 +147,20 @@ class EnterpriseAccessApiService { * List content assignments for a specific AssignmentConfiguration. */ static listContentAssignments(assignmentConfigurationUUID, options = {}) { - const params = new URLSearchParams({ + const { learnerState, ...optionsRest } = options; + const params = { page: 1, page_size: 25, // Only include assignments with allocated or errored states. The table should NOT // include assignments in the canceled or accepted states. state__in: 'allocated,errored', - ...snakeCaseObject(options), - }); - const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/?${params.toString()}`; + ...snakeCaseObject(optionsRest), + }; + if (learnerState) { + params.learner_state__in = learnerState; + } + const urlParams = new URLSearchParams(params); + const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/?${urlParams.toString()}`; return EnterpriseAccessApiService.apiClient().get(url); } diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index 7fd27b22a1..a6a5aeec7f 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -134,7 +134,23 @@ describe('EnterpriseAccessApiService', () => { }); }); - test('listContentAssignments calls enterprise-access to fetch content assignments', () => { + test('listContentAssignments calls enterprise-access to fetch content assignments with learner state filter', () => { + const options = { + learnerState: ['notifying', 'waiting'], + }; + EnterpriseAccessApiService.listContentAssignments(mockAssignmentConfigurationUUID, options); + const expectedParams = new URLSearchParams({ + page: 1, + page_size: 25, + state__in: 'allocated,errored', + learner_state__in: 'notifying,waiting', + }).toString(); + expect(axios.get).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/?${expectedParams}`, + ); + }); + + test('listContentAssignments calls enterprise-access to fetch content assignments without learner state filter', () => { EnterpriseAccessApiService.listContentAssignments(mockAssignmentConfigurationUUID); const expectedParams = new URLSearchParams({ page: 1, From 15b95a008593029ca0b37e414a20213b449e3ec1 Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Fri, 17 Nov 2023 22:20:25 +0000 Subject: [PATCH 075/124] fix: cleaning up sso self service wording and behaviors --- .../SettingsSSOTab/NewExistingSSOConfigs.jsx | 4 +- .../SettingsSSOTab/NewSSOConfigCard.jsx | 11 +- .../SettingsSSOTab/NewSSOConfigForm.jsx | 64 +++++--- .../settings/SettingsSSOTab/NewSSOStepper.jsx | 7 +- .../settings/SettingsSSOTab/index.jsx | 43 ++++-- .../SettingsSSOTab/steps/SSOConfigIDPStep.jsx | 2 +- .../tests/ExistingSSOConfigs.test.jsx | 52 +++++-- .../tests/NewExistingSSOConfigs.test.jsx | 1 + .../tests/NewSSOConfigCard.test.jsx | 87 ++++++++--- .../tests/NewSSOConfigForm.test.jsx | 146 +++++++++++++----- 10 files changed, 294 insertions(+), 123 deletions(-) diff --git a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx index 638f06f7a4..bdfc1b910f 100644 --- a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx +++ b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx @@ -19,7 +19,7 @@ const FRESH_CONFIG_POLLING_INTERVAL = 30000; const UPDATED_CONFIG_POLLING_INTERVAL = 2000; const NewExistingSSOConfigs = ({ - configs, refreshBool, setRefreshBool, enterpriseId, setPollingNetworkError, + configs, refreshBool, setRefreshBool, enterpriseId, setPollingNetworkError, setIsStepperOpen, }) => { const [inactiveConfigs, setInactiveConfigs] = useState([]); const [activeConfigs, setActiveConfigs] = useState([]); @@ -60,6 +60,7 @@ const NewExistingSSOConfigs = ({ setRefreshBool={setRefreshBool} refreshBool={refreshBool} setUpdateError={setUpdateError} + setIsStepperOpen={setIsStepperOpen} /> {updateError?.config === config.uuid && (
    @@ -197,6 +198,7 @@ NewExistingSSOConfigs.propTypes = { setRefreshBool: PropTypes.func.isRequired, enterpriseId: PropTypes.string.isRequired, setPollingNetworkError: PropTypes.func.isRequired, + setIsStepperOpen: PropTypes.func.isRequired, }; const mapStateToProps = state => ({ diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx index 984162305b..bcdacdaa2c 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx @@ -15,6 +15,7 @@ const NewSSOConfigCard = ({ setRefreshBool, refreshBool, setUpdateError, + setIsStepperOpen, }) => { const VALIDATED = config.validated_at; const ENABLED = config.active; @@ -23,6 +24,11 @@ const NewSSOConfigCard = ({ const { setProviderConfig } = useContext(SSOConfigContext); + const onConfigureClick = (selectedConfig) => { + setProviderConfig(selectedConfig); + setIsStepperOpen(true); + }; + const convertToReadableDate = (date) => { const dateObj = new Date(date); const options = { year: 'numeric', month: 'short', day: 'numeric' }; @@ -127,7 +133,7 @@ const NewSSOConfigCard = ({ {!VALIDATED && CONFIGURED && ( + @@ -92,4 +105,12 @@ const NoBudgetActivityEmptyState = () => { ); }; -export default NoBudgetActivityEmptyState; +NoBudgetActivityEmptyState.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; + +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(NoBudgetActivityEmptyState); diff --git a/src/components/learner-credit-management/cards/AssignmentAllocationHelpCollapsibles.jsx b/src/components/learner-credit-management/cards/AssignmentAllocationHelpCollapsibles.jsx new file mode 100644 index 0000000000..2ac97635cb --- /dev/null +++ b/src/components/learner-credit-management/cards/AssignmentAllocationHelpCollapsibles.jsx @@ -0,0 +1,98 @@ +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Collapsible, Stack } from '@edx/paragon'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import React from 'react'; +import { ASSIGNMENT_ENROLLMENT_DEADLINE } from '../data'; +import EVENT_NAMES from '../../../eventTracking'; + +const AssignmentAllocationHelpCollapsibles = ({ enterpriseId, course }) => ( + + Next steps for assigned learners} + defaultOpen + onToggle={(open) => { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.TOGGLE_NEXT_STEPS, + { isOpen: open }, + ); + }} + > +
    +
      +
    • + Learners will be notified of this course assignment by email. +
    • +
    • + Learners must complete enrollment for this assignment by {course.enrollmentDeadline}. This deadline + is calculated based on the course enrollment deadline or {ASSIGNMENT_ENROLLMENT_DEADLINE} days + past the date of assignment, whichever is sooner. +
    • +
    +
    +
    + Impact on your Learner Credit budget} + onToggle={(open) => { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.TOGGLE_IMPACT_ON_YOUR_LEARNERS, + { isOpen: open }, + ); + }} + > +
    +
      +
    • + The total assignment cost will be earmarked as "assigned" funds in your + Learner Credit budget so you can't overspend. +
    • +
    • + The course cost will automatically convert from "assigned" to "spent" funds + when your learners complete registration. +
    • +
    +
    +
    + Managing this assignment} + onToggle={(open) => { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.TOGGLE_MANAGING_THIS_ASSIGNMENT, + { isOpen: open }, + ); + }} + > +
    +
      +
    • + You will be able to monitor the status of this assignment by reviewing + your Learner Credit Budget activity. +
    • +
    • + You can cancel this course assignment or send email reminders any time + before learners complete enrollment. +
    • +
    +
    +
    +
    +); + +AssignmentAllocationHelpCollapsibles.propTypes = { + enterpriseId: PropTypes.string.isRequired, + course: PropTypes.shape({ + enrollmentDeadline: PropTypes.string.isRequired, + }).isRequired, +}; + +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentAllocationHelpCollapsibles); diff --git a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx index 0dab3dc809..a02f63016b 100644 --- a/src/components/learner-credit-management/cards/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/cards/AssignmentModalContent.jsx @@ -11,18 +11,20 @@ import { Form, Card, } from '@edx/paragon'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import BaseCourseCard from './BaseCourseCard'; import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from '../data'; -import { ImpactOnYourLearnerCreditBudget, ManagingThisAssignment, NextStepsForAssignedLearners } from './Collapsibles'; import AssignmentModalSummary from './AssignmentModalSummary'; import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isEmailAddressesInputValueValid } from './data'; +import AssignmentAllocationHelpCollapsibles from './AssignmentAllocationHelpCollapsibles'; +import EVENT_NAMES from '../../../eventTracking'; -const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { +const AssignmentModalContent = ({ enterpriseId, course, onEmailAddressesChange }) => { const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const spendAvailable = subsidyAccessPolicy.aggregates.spendAvailableUsd; - const [learnerEmails, setLearnerEmails] = useState([]); const [emailAddressesInputValue, setEmailAddressesInputValue] = useState(''); const [assignmentAllocationMetadata, setAssignmentAllocationMetadata] = useState({}); @@ -61,12 +63,19 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { contentPrice, }); setAssignmentAllocationMetadata(allocationMetadata); + if (allocationMetadata.validationError?.reason) { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.EMAIL_ADDRESS_VALIDATION, + { validationErrorReason: allocationMetadata.validationError.reason }, + ); + } if (allocationMetadata.canAllocate) { onEmailAddressesChange(learnerEmails, { canAllocate: true }); } else { onEmailAddressesChange([]); } - }, [onEmailAddressesChange, learnerEmails, contentPrice, spendAvailable]); + }, [onEmailAddressesChange, learnerEmails, contentPrice, spendAvailable, enterpriseId]); return ( @@ -100,11 +109,7 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { )}
    How assigning this course works
    - - - - - +

    Pay by Learner Credit

    @@ -151,8 +156,13 @@ const AssignmentModalContent = ({ course, onEmailAddressesChange }) => { }; AssignmentModalContent.propTypes = { + enterpriseId: PropTypes.string.isRequired, course: PropTypes.shape().isRequired, // Pass-thru prop to `BaseCourseCard` onEmailAddressesChange: PropTypes.func.isRequired, }; -export default AssignmentModalContent; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentModalContent); diff --git a/src/components/learner-credit-management/cards/Collapsibles.jsx b/src/components/learner-credit-management/cards/Collapsibles.jsx deleted file mode 100644 index 501b113dcf..0000000000 --- a/src/components/learner-credit-management/cards/Collapsibles.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Collapsible } from '@edx/paragon'; - -import { ASSIGNMENT_ENROLLMENT_DEADLINE } from '../data'; - -export const NextStepsForAssignedLearners = ({ course }) => ( - Next steps for assigned learners} - defaultOpen - > -
    -
      -
    • - Learners will be notified of this course assignment by email. -
    • -
    • - Learners must complete enrollment for this assignment by {course.enrollmentDeadline}. This deadline - is calculated based on the course enrollment deadline or {ASSIGNMENT_ENROLLMENT_DEADLINE} days - past the date of assignment, whichever is sooner. -
    • -
    -
    -
    -); - -NextStepsForAssignedLearners.propTypes = { - course: PropTypes.shape({ - enrollmentDeadline: PropTypes.string.isRequired, - }).isRequired, -}; - -export const ImpactOnYourLearnerCreditBudget = () => ( - Impact on your Learner Credit budget} - > -
    -
      -
    • - The total assignment cost will be earmarked as "assigned" funds in your - Learner Credit budget so you can't overspend. -
    • -
    • - The course cost will automatically convert from "assigned" to "spent" funds - when your learners complete registration. -
    • -
    -
    -
    -); - -export const ManagingThisAssignment = () => ( - Managing this assignment} - > -
    -
      -
    • - You will be able to monitor the status of this assignment by reviewing - your Learner Credit Budget activity. -
    • -
    • - You can cancel this course assignment or send email reminders any time - before learners complete enrollment. -
    • -
    -
    -
    -); diff --git a/src/components/learner-credit-management/cards/CourseCard.jsx b/src/components/learner-credit-management/cards/CourseCard.jsx index 2e1ec52cdb..90f78cc6b6 100644 --- a/src/components/learner-credit-management/cards/CourseCard.jsx +++ b/src/components/learner-credit-management/cards/CourseCard.jsx @@ -1,32 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Hyperlink } from '@edx/paragon'; -import NewAssignmentModalButton from './NewAssignmentModalButton'; -import CARD_TEXT from '../constants'; import BaseCourseCard from './BaseCourseCard'; - -const { BUTTON_ACTION } = CARD_TEXT; - -const CourseCardFooterActions = ({ course }) => { - const { linkToCourse } = course; - - return [ - , - - {BUTTON_ACTION.assign} - , - ]; -}; +import CourseCardFooterActions from './CourseCardFooterActions'; const CourseCard = ({ original }) => ( { + const { linkToCourse, uuid } = course; + const handleViewCourse = () => { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.VIEW_COURSE, + { courseUUID: uuid }, + ); + }; + return [ + , + + {BUTTON_ACTION.assign} + , + ]; +}; + +CourseCardFooterActions.propTypes = { + enterpriseId: PropTypes.string.isRequired, + course: PropTypes.shape().isRequired, +}; + +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(CourseCardFooterActions); diff --git a/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx index 6bec89834a..dbf371d078 100644 --- a/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx +++ b/src/components/learner-credit-management/cards/CreateAllocationErrorAlertModals.jsx @@ -1,11 +1,15 @@ import React, { useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useToggle } from '@edx/paragon'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import SystemErrorAlertModal from './assignment-allocation-status-modals/SystemErrorAlertModal'; import ContentNotInCatalogErrorAlertModal from './assignment-allocation-status-modals/ContentNotInCatalogErrorAlertModal'; import NotEnoughBalanceAlertModal from './assignment-allocation-status-modals/NotEnoughBalanceAlertModal'; +import EVENT_NAMES from '../../../eventTracking'; const CreateAllocationErrorAlertModals = ({ + enterpriseId, errorReason, retry, closeAssignmentModal, @@ -24,6 +28,15 @@ const CreateAllocationErrorAlertModals = ({ }); }, [closeCatalogErrorModal, closeBalanceErrorModal, closeSystemErrorModal]); + const closeAssignmentModalWithTrackEvent = () => { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_ASSIGNMENT_ALLOCATION_ERROR, + { errorReason }, + ); + closeAssignmentModal(); + }; + /** * Retry the original action that caused the error and close all error modals. */ @@ -57,18 +70,18 @@ const CreateAllocationErrorAlertModals = ({ @@ -76,9 +89,14 @@ const CreateAllocationErrorAlertModals = ({ }; CreateAllocationErrorAlertModals.propTypes = { + enterpriseId: PropTypes.string.isRequired, closeAssignmentModal: PropTypes.func.isRequired, retry: PropTypes.func.isRequired, errorReason: PropTypes.string, }; -export default CreateAllocationErrorAlertModals; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(CreateAllocationErrorAlertModals); diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 2dee3ff00a..353af471e3 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -9,23 +9,29 @@ import { Hyperlink, StatefulButton, } from '@edx/paragon'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { snakeCaseObject } from '@edx/frontend-platform/utils'; +import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform/utils'; +import { connect } from 'react-redux'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys, useBudgetId } from '../data'; import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals'; import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; +import EVENT_NAMES from '../../../eventTracking'; const useAllocateContentAssignments = () => useMutation({ mutationFn: async ({ subsidyAccessPolicyId, payload, - }) => EnterpriseAccessApiService.allocateContentAssignments(subsidyAccessPolicyId, payload), + }) => { + const response = await EnterpriseAccessApiService.allocateContentAssignments(subsidyAccessPolicyId, payload); + return camelCaseObject(response.data); + }, }); -const NewAssignmentModalButton = ({ course, children }) => { +const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const history = useHistory(); const routeMatch = useRouteMatch(); const queryClient = useQueryClient(); @@ -41,6 +47,17 @@ const NewAssignmentModalButton = ({ course, children }) => { const pathToActivityTab = generatePath(routeMatch.path, { budgetId: subsidyAccessPolicyId, activeTabKey: 'activity' }); + const handleOpenAssignmentModal = () => { + open(); + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_ASSIGN_COURSE, + { + isOpen: !isOpen, + courseUUID: course.uuid, + }, + ); + }; const handleCloseAssignmentModal = () => { close(); setAssignButtonState('default'); @@ -57,6 +74,21 @@ const NewAssignmentModalButton = ({ course, children }) => { setCanAllocateAssignments(canAllocate); }, []); + const onSuccessEnterpriseTrackEvents = ({ created, noChange, updated }) => { + const trackEventMetadata = { + totalAllocatedLearners: learnerEmails.length, + created: created.length, + noChange: noChange.length, + updated: updated.length, + courseUUID: course.uuid, + }; + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_ALLOCATION_LEARNER_ASSIGNMENT, + trackEventMetadata, + ); + }; + const handleAllocateContentAssignments = () => { const payload = snakeCaseObject({ contentPriceCents: course.normalizedMetadata.contentPrice * 100, // Convert to USD cents @@ -70,12 +102,13 @@ const NewAssignmentModalButton = ({ course, children }) => { setAssignButtonState('pending'); setCreateAssignmentsErrorReason(null); mutate(mutationArgs, { - onSuccess: () => { + onSuccess: ({ created, noChange, updated }) => { setAssignButtonState('complete'); queryClient.invalidateQueries({ queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), }); handleCloseAssignmentModal(); + onSuccessEnterpriseTrackEvents({ created, noChange, updated }); displayToastForAssignmentAllocation({ totalLearnersAssigned: learnerEmails.length }); history.push(pathToActivityTab); }, @@ -84,32 +117,72 @@ const NewAssignmentModalButton = ({ course, children }) => { httpErrorStatus, httpErrorResponseData, } = err.customAttributes; + let errorReason = 'system_error'; if (httpErrorStatus === 422) { const responseData = JSON.parse(httpErrorResponseData); - setCreateAssignmentsErrorReason(responseData[0].reason); + errorReason = responseData[0].reason; + setCreateAssignmentsErrorReason(errorReason); } else { - setCreateAssignmentsErrorReason('system_error'); + setCreateAssignmentsErrorReason(errorReason); } setAssignButtonState('error'); + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_ALLOCATION_ERROR, + { + totalAllocatedLearners: learnerEmails.length, + courseUUID: course.uuid, + errorStatus: httpErrorStatus, + errorReason, + }, + ); }, }); }; return ( <> - + { + handleCloseAssignmentModal(); + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_EXIT, + { assignButtonState }, + ); + }} footerNode={( - - + { }; NewAssignmentModalButton.propTypes = { + enterpriseId: PropTypes.string.isRequired, course: PropTypes.shape().isRequired, // Pass-thru prop to `BaseCourseCard` children: PropTypes.node.isRequired, // Represents the button text }; -export default NewAssignmentModalButton; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(NewAssignmentModalButton); diff --git a/src/components/learner-credit-management/cards/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx similarity index 89% rename from src/components/learner-credit-management/cards/CourseCard.test.jsx rename to src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 00bf58ecc5..38d4d5267a 100644 --- a/src/components/learner-credit-management/cards/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -8,32 +8,37 @@ import configureMockStore from 'redux-mock-store'; import { QueryClientProvider, useQueryClient } from '@tanstack/react-query'; import { AppContext } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { renderWithRouter } from '@edx/frontend-enterprise-utils'; +import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; -import CourseCard from './CourseCard'; +import CourseCard from '../CourseCard'; import { formatPrice, learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy, -} from '../data'; -import { getButtonElement, queryClient } from '../../test/testUtils'; +} from '../../data'; +import { getButtonElement, queryClient } from '../../../test/testUtils'; -import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; -import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; -import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from './data'; +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import { BudgetDetailPageContext } from '../../BudgetDetailPageWrapper'; +import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from '../data'; + +jest.mock('@edx/frontend-enterprise-utils', () => ({ + ...jest.requireActual('@edx/frontend-enterprise-utils'), + sendEnterpriseTrackEvent: jest.fn(), +})); jest.mock('@tanstack/react-query', () => ({ ...jest.requireActual('@tanstack/react-query'), useQueryClient: jest.fn(), })); -jest.mock('../data', () => ({ - ...jest.requireActual('../data'), +jest.mock('../../data', () => ({ + ...jest.requireActual('../../data'), useBudgetId: jest.fn(), useSubsidyAccessPolicy: jest.fn(), })); -jest.mock('../../../data/services/EnterpriseAccessApiService'); +jest.mock('../../../../data/services/EnterpriseAccessApiService'); const originalData = { availability: ['Upcoming'], @@ -216,6 +221,45 @@ describe('Course card works as expected', () => { expect(viewCourseCTA.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/executive-education-2u/course/exec-ed-course-123x'); }); + test('view course sends segment events', () => { + renderWithRouter(); + const viewCourseCTA = screen.getByText('View course', { selector: 'a' }); + userEvent.click(viewCourseCTA); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + }); + + test('card exits and sends segment events', () => { + renderWithRouter(); + + const assignCourseCTA = getButtonElement('Assign'); + expect(assignCourseCTA).toBeInTheDocument(); + userEvent.click(assignCourseCTA); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + + const assignmentModal = within(screen.getByRole('dialog')); + expect(assignmentModal.getByText('Assign this course')).toBeInTheDocument(); + + const closeButton = screen.getByRole('button', { name: 'Close' }); + userEvent.click(closeButton); + + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + }); + + test('help center article link sends segment events', () => { + renderWithRouter(); + + const assignCourseCTA = getButtonElement('Assign'); + expect(assignCourseCTA).toBeInTheDocument(); + userEvent.click(assignCourseCTA); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + + const helpCenterButton = screen.getByText('Help Center: Course Assignments'); + + expect(helpCenterButton).toBeInTheDocument(); + userEvent.click(helpCenterButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + }); + test.each([ { shouldSubmitAssignments: true, @@ -346,15 +390,20 @@ describe('Course card works as expected', () => { expect(assignmentModal.getByText('Learners will be notified of this course assignment by email.')).toBeInTheDocument(); const budgetImpact = assignmentModal.getByText('Impact on your Learner Credit budget'); expect(budgetImpact).toBeInTheDocument(); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); expect(assignmentModal.queryByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).not.toBeInTheDocument(); userEvent.click(budgetImpact); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); expect(assignmentModal.getByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).toBeInTheDocument(); const managingAssignment = assignmentModal.getByText('Managing this assignment'); expect(managingAssignment).toBeInTheDocument(); expect(assignmentModal.queryByText('You will be able to monitor the status of this assignment', { exact: false })).not.toBeInTheDocument(); userEvent.click(managingAssignment); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(3); expect(assignmentModal.getByText('You will be able to monitor the status of this assignment', { exact: false })).toBeInTheDocument(); - + const nextSteps = assignmentModal.getByText('Next steps for assigned learners'); + userEvent.click(nextSteps); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(4); // Verify modal footer expect(assignmentModal.getByText('Help Center: Course Assignments')).toBeInTheDocument(); const cancelAssignmentCTA = getButtonElement('Cancel', { screenOverride: assignmentModal }); @@ -426,8 +475,10 @@ describe('Course card works as expected', () => { expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); if (shouldRetryAllocationAfterException) { await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); + expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); } else { await simulateClickErrorModalExit(assignmentErrorModal); + expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); } } } else { diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index f448267938..11dd744498 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -1,3 +1,20 @@ +/* START LOCAL TESTING CONSTANTS */ +// Set to false before pushing PR! otherwise set to true to enable local testing of learner-credit-management components +// Test will fail as additional check to ensure this is set to false before pushing PR +export const TEST_FLAG = false; +// Test enterpriseCatalogUuid for learner-credit-management search +// to display card selections and confirmation +export const testEnterpriseCatalogUuid = 'e3107bf4-2eac-4307-a049-cc691ea7213b '; +// function that passes through enterpriseCatalogUuid if TEST_FLAG is false, otherwise +// returns local testing enterpriseCatalogUuid +export const ENABLE_TESTING = (enterpriseCatalogUuid, enableTest = TEST_FLAG) => { + if (enableTest) { + return testEnterpriseCatalogUuid; + } + return enterpriseCatalogUuid; +}; +/* END LOCAL TESTING CONSTANTS */ + export const API_FIELDS_BY_TABLE_COLUMN_ACCESSOR = { courseTitle: 'course_title', enrollmentDate: 'enrollment_date', diff --git a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js b/src/components/learner-credit-management/data/hooks/tests/useBudgetContentAssignments.test.js similarity index 71% rename from src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js rename to src/components/learner-credit-management/data/hooks/tests/useBudgetContentAssignments.test.js index 27d7aef092..f05fc7cfd3 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.test.js +++ b/src/components/learner-credit-management/data/hooks/tests/useBudgetContentAssignments.test.js @@ -1,13 +1,23 @@ import { renderHook } from '@testing-library/react-hooks'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; -import useBudgetContentAssignments from './useBudgetContentAssignments'; -import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import useBudgetContentAssignments from '../useBudgetContentAssignments'; +import EnterpriseAccessApiService from '../../../../../data/services/EnterpriseAccessApiService'; + +jest.mock('@edx/frontend-enterprise-utils', () => ({ + ...jest.requireActual('@edx/frontend-enterprise-utils'), + sendEnterpriseTrackEvent: jest.fn(), +})); describe('useBudgetContentAssignments', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('does not call fetchContentAssignments if isEnabled is false', async () => { const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ assignmentConfigurationUUID: '123', isEnabled: false, + enterpriseId: 'test-enterprise-id', })); const { fetchContentAssignments } = result.current; const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); @@ -34,6 +44,7 @@ describe('useBudgetContentAssignments', () => { const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ assignmentConfigurationUUID: '123', isEnabled: true, + enterpriseId: 'test-enterprise-id', })); const { fetchContentAssignments } = result.current; const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); @@ -91,10 +102,13 @@ describe('useBudgetContentAssignments', () => { ], hasSearchParam: false, }, - ])('handles assignment details filter with search query parameter (%s)', async ({ filters, hasSearchParam }) => { + ])('handles assignment details filter with search query parameter (%s)', async ({ + filters, hasSearchParam, + }) => { const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ assignmentConfigurationUUID: '123', isEnabled: true, + enterpriseId: 'test-enterprise-id', })); const { fetchContentAssignments } = result.current; const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); @@ -118,7 +132,6 @@ describe('useBudgetContentAssignments', () => { }); await waitForNextUpdate(); - expect(mockListContentAssignments).toHaveBeenCalledWith( '123', { @@ -152,6 +165,7 @@ describe('useBudgetContentAssignments', () => { const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ assignmentConfigurationUUID: '123', isEnabled: true, + enterpriseId: 'test-enterprise-id', })); const { fetchContentAssignments } = result.current; const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); @@ -185,7 +199,6 @@ describe('useBudgetContentAssignments', () => { }, ); }); - it.each([ { sortBy: [ @@ -247,6 +260,7 @@ describe('useBudgetContentAssignments', () => { const { result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments({ assignmentConfigurationUUID: '123', isEnabled: true, + enterpriseId: 'test-enterprise-id', })); const { fetchContentAssignments } = result.current; const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); @@ -270,7 +284,6 @@ describe('useBudgetContentAssignments', () => { }); await waitForNextUpdate(); - expect(mockListContentAssignments).toHaveBeenCalledWith( '123', { @@ -280,4 +293,65 @@ describe('useBudgetContentAssignments', () => { }, ); }); + it('calls enterprise track event', async () => { + const mockUseBudgetContentAssignmentsData = { + assignmentConfigurationUUID: '123', + isEnabled: true, + enterpriseId: 'test-enterprise-id', + }; + const mockListContentAssignmentsData = { + data: { + results: [ + { + id: 1, + name: 'test', + }, + ], + count: 1, + numPages: 1, + currentPage: 1, + }, + }; + const initialSortByMetadata = { + id: 'amount', + desc: true, + }; + const modifiedSortByMetaData = { + id: 'amount', + desc: false, + }; + + // Perform first render where currentArgsRef.current = null, no track event called + const { rerender, result, waitForNextUpdate } = renderHook(() => useBudgetContentAssignments( + mockUseBudgetContentAssignmentsData, + )); + + const { fetchContentAssignments } = result.current; + const mockListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + mockListContentAssignments.mockResolvedValue(mockListContentAssignmentsData); + await fetchContentAssignments({ + pageIndex: 0, + pageSize: 10, + sortBy: [initialSortByMetadata], + }); + + await waitForNextUpdate(); + + expect(sendEnterpriseTrackEvent).not.toHaveBeenCalled(); + + // Performs a `rerender` of the first renderHook call after the currentArgsRef.current has been hydrated + rerender(mockUseBudgetContentAssignmentsData); + + const mockSecondListContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'listContentAssignments'); + mockSecondListContentAssignments.mockResolvedValue(mockListContentAssignmentsData); + await fetchContentAssignments({ + pageIndex: 0, + pageSize: 10, + sortBy: [modifiedSortByMetaData], + }); + + await waitForNextUpdate(); + + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx b/src/components/learner-credit-management/data/hooks/tests/useBudgetDetailActivityOverview.test.jsx similarity index 89% rename from src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx rename to src/components/learner-credit-management/data/hooks/tests/useBudgetDetailActivityOverview.test.jsx index 351f8b5cf4..ebe6ad7b9e 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.test.jsx +++ b/src/components/learner-credit-management/data/hooks/tests/useBudgetDetailActivityOverview.test.jsx @@ -1,22 +1,22 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; -import useBudgetDetailActivityOverview from './useBudgetDetailActivityOverview'; -import useBudgetId from './useBudgetId'; -import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; -import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; -import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; +import useBudgetDetailActivityOverview from '../useBudgetDetailActivityOverview'; +import useBudgetId from '../useBudgetId'; +import useSubsidyAccessPolicy from '../useSubsidyAccessPolicy'; +import EnterpriseAccessApiService from '../../../../../data/services/EnterpriseAccessApiService'; +import EnterpriseDataApiService from '../../../../../data/services/EnterpriseDataApiService'; import { mockAssignableSubsidyAccessPolicy, mockPerLearnerSpendLimitSubsidyAccessPolicy, mockEnterpriseOfferId, mockSubsidyAccessPolicyUUID, -} from '../tests/constants'; -import { queryClient } from '../../../test/testUtils'; -import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; +} from '../../tests/constants'; +import { queryClient } from '../../../../test/testUtils'; +import SubsidyApiService from '../../../../../data/services/EnterpriseSubsidyApiService'; -jest.mock('./useBudgetId'); -jest.mock('./useSubsidyAccessPolicy'); +jest.mock('../useBudgetId'); +jest.mock('../useSubsidyAccessPolicy'); const mockEnterpriseUUID = 'mock-enterprise-uuid'; diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx b/src/components/learner-credit-management/data/hooks/tests/useOfferRedemptions.test.jsx similarity index 88% rename from src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx rename to src/components/learner-credit-management/data/hooks/tests/useOfferRedemptions.test.jsx index 0738b5bf1a..5e8d29a495 100644 --- a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.test.jsx +++ b/src/components/learner-credit-management/data/hooks/tests/useOfferRedemptions.test.jsx @@ -2,11 +2,11 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook } from '@testing-library/react-hooks/dom'; import { camelCaseObject } from '@edx/frontend-platform/utils'; -import useOfferRedemptions from './useOfferRedemptions'; -import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; -import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; -import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; -import { queryClient } from '../../../test/testUtils'; +import useOfferRedemptions from '../useOfferRedemptions'; +import useSubsidyAccessPolicy from '../useSubsidyAccessPolicy'; +import EnterpriseDataApiService from '../../../../../data/services/EnterpriseDataApiService'; +import SubsidyApiService from '../../../../../data/services/EnterpriseSubsidyApiService'; +import { queryClient } from '../../../../test/testUtils'; const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; const TEST_ENTERPRISE_OFFER_ID = 1; @@ -49,9 +49,9 @@ const mockEnterpriseOffer = { id: TEST_ENTERPRISE_OFFER_ID, }; -jest.mock('./useSubsidyAccessPolicy'); -jest.mock('../../../../data/services/EnterpriseDataApiService'); -jest.mock('../../../../data/services/EnterpriseSubsidyApiService'); +jest.mock('../useSubsidyAccessPolicy'); +jest.mock('../../../../../data/services/EnterpriseDataApiService'); +jest.mock('../../../../../data/services/EnterpriseSubsidyApiService'); const wrapper = ({ children }) => ( {children} diff --git a/src/components/learner-credit-management/data/hooks/useOfferSummary.test.js b/src/components/learner-credit-management/data/hooks/tests/useOfferSummary.test.js similarity index 89% rename from src/components/learner-credit-management/data/hooks/useOfferSummary.test.js rename to src/components/learner-credit-management/data/hooks/tests/useOfferSummary.test.js index 352b104f6b..40ccc6df01 100644 --- a/src/components/learner-credit-management/data/hooks/useOfferSummary.test.js +++ b/src/components/learner-credit-management/data/hooks/tests/useOfferSummary.test.js @@ -1,14 +1,14 @@ import { renderHook } from '@testing-library/react-hooks/dom'; -import useOfferSummary from './useOfferSummary'; -import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; +import useOfferSummary from '../useOfferSummary'; +import EnterpriseDataApiService from '../../../../../data/services/EnterpriseDataApiService'; jest.mock('@edx/frontend-platform/config', () => ({ getConfig: jest.fn(() => ({ FEATURE_LEARNER_CREDIT_MANAGEMENT: true, })), })); -jest.mock('../../../../data/services/EnterpriseDataApiService'); +jest.mock('../../../../../data/services/EnterpriseDataApiService'); const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; const TEST_ENTERPRISE_OFFER_ID = 1; diff --git a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx b/src/components/learner-credit-management/data/hooks/tests/useSubsidyAccessPolicy.test.jsx similarity index 92% rename from src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx rename to src/components/learner-credit-management/data/hooks/tests/useSubsidyAccessPolicy.test.jsx index cc773fcb79..1d654bd428 100644 --- a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.test.jsx +++ b/src/components/learner-credit-management/data/hooks/tests/useSubsidyAccessPolicy.test.jsx @@ -1,15 +1,15 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; -import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; // Import the hook -import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; -import { queryClient } from '../../../test/testUtils'; +import useSubsidyAccessPolicy from '../useSubsidyAccessPolicy'; // Import the hook +import EnterpriseAccessApiService from '../../../../../data/services/EnterpriseAccessApiService'; +import { queryClient } from '../../../../test/testUtils'; const mockSubsidyAccessPolicyUUID = '9af340a9-48de-4d94-976d-e2282b9eb7f3'; const mockAssignmentConfiguration = { uuid: 'test-assignment-configuration-uuid' }; // Mock the EnterpriseAccessApiService -jest.mock('../../../../data/services/EnterpriseAccessApiService', () => ({ +jest.mock('../../../../../data/services/EnterpriseAccessApiService', () => ({ retrieveSubsidyAccessPolicy: jest.fn().mockResolvedValue({ data: { uuid: '9af340a9-48de-4d94-976d-e2282b9eb7f3', diff --git a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js index 8bd33e9a07..ff9c15d22d 100644 --- a/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js +++ b/src/components/learner-credit-management/data/hooks/useBudgetContentAssignments.js @@ -1,8 +1,12 @@ -import { useCallback, useMemo, useState } from 'react'; +import { + useCallback, useMemo, useState, useRef, +} from 'react'; import debounce from 'lodash.debounce'; import { camelCaseObject } from '@edx/frontend-platform/utils'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import EVENT_NAMES from '../../../../eventTracking'; const initialContentAssignmentsState = { results: [], @@ -12,7 +16,7 @@ const initialContentAssignmentsState = { learnerStateCounts: [], }; -const applyFiltersToOptions = (filters, options) => { +export const applyFiltersToOptions = (filters, options) => { if (!filters || filters.length === 0) { return; } @@ -26,7 +30,7 @@ const applyFiltersToOptions = (filters, options) => { } }; -const applySortByToOptions = (sortBy, options) => { +export const applySortByToOptions = (sortBy, options) => { if (!sortBy || sortBy.length === 0) { return; } @@ -44,7 +48,7 @@ const applySortByToOptions = (sortBy, options) => { const apiFieldKey = apiFieldForColumnAccessor.key; // Determine whether the API field ordering should be reversed based on the column accessor. This is // necessary because the content_quantity field is a negative number, but if the column is sorted in a - // descending order, users would likely expect the larger contenr quantity to be at the top of the list, + // descending order, users would likely expect the larger content quantity to be at the top of the list, // which is technically the smaller number since its negative. if (isApiFieldOrderingReversed) { return desc ? apiFieldKey : `-${apiFieldKey}`; @@ -59,10 +63,11 @@ const applySortByToOptions = (sortBy, options) => { const useBudgetContentAssignments = ({ assignmentConfigurationUUID, isEnabled, + enterpriseId, }) => { + const currentArgsRef = useRef(null); const [isLoading, setIsLoading] = useState(true); const [contentAssignments, setContentAssignments] = useState(initialContentAssignmentsState); - const fetchContentAssignments = useCallback((args) => { if (!isEnabled || !assignmentConfigurationUUID) { setIsLoading(false); @@ -76,15 +81,45 @@ const useBudgetContentAssignments = ({ }; applyFiltersToOptions(args.filters, options); applySortByToOptions(args.sortBy, options); + + /* This logic in conjunction with useRef is being used to prevent track events + from being called when the page re-renders without the specifically selected + arguments (argCopy) being changed */ + const argsCopy = { + pageIndex: args.pageIndex, + pageSize: args.pageSize, + filters: args.filters, + sortBy: args.sortBy, + }; + const shouldEmitSegmentEvent = !!currentArgsRef.current && ( + JSON.stringify(argsCopy) !== JSON.stringify(currentArgsRef.current)); + if (shouldEmitSegmentEvent) { + const trackEventMetadata = { + filters: { + learnerState: options.learnerState || null, + search: options.search || null, + }, + ordering: options.ordering || null, + page: options.page || null, + pageSize: options.pageSize || null, + }; + await sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_DETAILS_ASSIGNED_DATATABLE_SORT_BY_OR_FILTER, + trackEventMetadata, + ); + } const assignmentsResponse = await EnterpriseAccessApiService.listContentAssignments( assignmentConfigurationUUID, options, ); setContentAssignments(camelCaseObject(assignmentsResponse.data)); setIsLoading(false); + // Memoizes argsCopy to be referenced against future re-renders + currentArgsRef.current = argsCopy; }; getContentAssignments(); - }, [isEnabled, assignmentConfigurationUUID]); + }, [isEnabled, assignmentConfigurationUUID, enterpriseId]); const debouncedFetchContentAssigments = useMemo( () => debounce(fetchContentAssignments, 300), diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js index 2898577a0a..64618cae8b 100644 --- a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js +++ b/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js @@ -14,6 +14,7 @@ import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiSer import { API_FIELDS_BY_TABLE_COLUMN_ACCESSOR } from '../constants'; import { transformUtilizationTableResults, transformUtilizationTableSubsidyTransactionResults } from '../utils'; import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; +import EVENT_NAMES from '../../../../eventTracking'; const applySortByToOptions = (sortBy, options) => { const orderingStrings = sortBy.map(({ id, desc }) => { @@ -113,7 +114,7 @@ const useOfferRedemptions = ( // send all table state as event properties. sendEnterpriseTrackEvent( enterpriseUUID, - 'edx.ui.enterprise.admin_portal.learner-credit-management.table.data.changed', + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_DETAILS_SPENT_DATATABLE_SORT_BY_OR_FILTER, options, ); } else { diff --git a/src/components/learner-credit-management/data/tests/constants.test.js b/src/components/learner-credit-management/data/tests/constants.test.js new file mode 100644 index 0000000000..9c5faf8131 --- /dev/null +++ b/src/components/learner-credit-management/data/tests/constants.test.js @@ -0,0 +1,20 @@ +import { + TEST_FLAG, + ENABLE_TESTING, + testEnterpriseCatalogUuid, +} from '../constants'; + +const enterpriseCatalogUuid = 'test-enterprise-catalogUuid'; + +describe('constants', () => { + it('should be defined', () => { + expect(testEnterpriseCatalogUuid).toBeDefined(); + }); + it('ENABLE_TESTING should pass through when the TEST_FLAG = false', () => { + expect(TEST_FLAG).toBe(false); + expect(ENABLE_TESTING(enterpriseCatalogUuid)).toBe(enterpriseCatalogUuid); + }); + it('ENABLE_TESTING should return the testEnterpriseId when passing true parameter', () => { + expect(ENABLE_TESTING(testEnterpriseCatalogUuid, true)).toBe(testEnterpriseCatalogUuid); + }); +}); diff --git a/src/components/learner-credit-management/search/CatalogSearch.jsx b/src/components/learner-credit-management/search/CatalogSearch.jsx index bde723051e..a557a66dc3 100644 --- a/src/components/learner-credit-management/search/CatalogSearch.jsx +++ b/src/components/learner-credit-management/search/CatalogSearch.jsx @@ -7,7 +7,9 @@ import { SearchHeader } from '@edx/frontend-enterprise-catalog-search'; import { configuration } from '../../../config'; import CatalogSearchResults from './CatalogSearchResults'; -import { SEARCH_RESULT_PAGE_SIZE, useBudgetId, useSubsidyAccessPolicy } from '../data'; +import { + ENABLE_TESTING, SEARCH_RESULT_PAGE_SIZE, useBudgetId, useSubsidyAccessPolicy, +} from '../data'; const CatalogSearch = () => { const searchClient = algoliasearch(configuration.ALGOLIA.APP_ID, configuration.ALGOLIA.SEARCH_API_KEY); @@ -15,9 +17,8 @@ const CatalogSearch = () => { const { data: subsidyAccessPolicy, } = useSubsidyAccessPolicy(subsidyAccessPolicyId); - const searchFilters = `enterprise_catalog_uuids:${subsidyAccessPolicy.catalogUuid} AND content_type:course`; + const searchFilters = `enterprise_catalog_uuids:${ENABLE_TESTING(subsidyAccessPolicy.catalogUuid)} AND content_type:course`; const displayName = subsidyAccessPolicy.displayName ? `${subsidyAccessPolicy.displayName} catalog` : 'Overview'; - return (
    ); } - return (
    ({ ...jest.requireActual('react-instantsearch-dom'), @@ -18,7 +18,7 @@ jest.mock('react-instantsearch-dom', () => ({ Index: () =>
    SEARCH
    , })); -jest.mock('../data'); +jest.mock('../../data'); const DEFAULT_SEARCH_CONTEXT_VALUE = { refinements: {} }; diff --git a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx similarity index 83% rename from src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx rename to src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx index 0143e8d5ba..60107dbfa5 100644 --- a/src/components/learner-credit-management/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx @@ -9,10 +9,10 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { QueryClientProvider } from '@tanstack/react-query'; -import { BaseCatalogSearchResults } from '../search/CatalogSearchResults'; -import { CONTENT_TYPE_COURSE } from '../data/constants'; -import { queryClient } from '../../test/testUtils'; -import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; +import { BaseCatalogSearchResults } from '../CatalogSearchResults'; +import { CONTENT_TYPE_COURSE } from '../../data/constants'; +import { queryClient } from '../../../test/testUtils'; +import { BudgetDetailPageContext } from '../../BudgetDetailPageWrapper'; // Mocking this connected component so as not to have to mock the algolia Api const PAGINATE_ME = 'PAGINATE ME :)'; @@ -25,8 +25,8 @@ jest.mock('react-instantsearch-dom', () => ({ Index: () =>
    Popular Courses
    , })); -jest.mock('../data', () => ({ - ...jest.requireActual('../data'), +jest.mock('../../data', () => ({ + ...jest.requireActual('../../data'), useSubsidyAccessPolicy: jest.fn().mockReturnValue({ data: { uuid: 'test-uuid', @@ -183,11 +183,35 @@ describe('Main Catalogs view works as expected', () => { - , , ); expect(screen.queryByText(TEST_COURSE_NAME)).toBeInTheDocument(); expect(screen.queryByText(TEST_COURSE_NAME_2)).toBeInTheDocument(); expect(screen.getAllByText('Showing 2 of 2.')[0]).toBeInTheDocument(); }); + test('error state displays', async () => { + const budgetDetailPageContextValue = { + isSuccessfulAssignmentAllocationToastOpen: false, + totalLearnersAssigned: undefined, + displayToastForAssignmentAllocation: jest.fn(), + closeToastForAssignmentAllocation: jest.fn(), + }; + const error = { + message: 'Your test has Failed', + }; + const errorState = { + ...defaultProps, + error, + }; + renderWithRouter( + + + + + + + , + ); + expect(screen.getByText(error.message, { exact: false })).toBeInTheDocument(); + }); }); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 41c4ffc760..a309d59968 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -8,7 +8,7 @@ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { renderWithRouter } from '@edx/frontend-enterprise-utils'; +import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { act } from 'react-dom/test-utils'; import BudgetDetailPage from '../BudgetDetailPage'; @@ -32,6 +32,11 @@ import { } from '../data/tests/constants'; import { getButtonElement, queryClient } from '../../test/testUtils'; +jest.mock('@edx/frontend-enterprise-utils', () => ({ + ...jest.requireActual('@edx/frontend-enterprise-utils'), + sendEnterpriseTrackEvent: jest.fn(), +})); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: jest.fn(), @@ -205,7 +210,7 @@ describe('', () => { it.each([ { isLargeViewport: true }, { isLargeViewport: false }, - ])('displays budget activity overview empty state', ({ isLargeViewport }) => { + ])('displays budget activity overview empty state', async ({ isLargeViewport }) => { useIsLargeOrGreater.mockReturnValue(isLargeViewport); useParams.mockReturnValue({ budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', @@ -226,6 +231,8 @@ describe('', () => { const illustrationTestIds = ['find-the-right-course-illustration', 'name-your-learners-illustration', 'confirm-spend-illustration']; illustrationTestIds.forEach(testId => expect(screen.getByTestId(testId)).toBeInTheDocument()); expect(screen.getByText('Get started', { selector: 'a' })).toBeInTheDocument(); + userEvent.click(screen.getByText('Get started')); + await waitFor(() => expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1)); }); it.each([ @@ -404,6 +411,7 @@ describe('', () => { userEvent.click(refreshCTA); expect(mockFetchContentAssignments).toHaveBeenCalledTimes(2); // should be called again on refresh expect(mockFetchContentAssignments).toHaveBeenLastCalledWith(expect.objectContaining(expectedTableFetchDataArgs)); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); }); it.each([ diff --git a/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx index 692405c4ed..bb4b0f31ea 100644 --- a/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx +++ b/src/components/learner-credit-management/tests/LearnerCreditAllocationTable.test.jsx @@ -138,4 +138,21 @@ describe('', () => { const courseTitleElement = screen.queryByText('course-title'); expect(courseTitleElement.closest('a')).toBeNull(); }); + it('displays isLoading state component', () => { + const store = mockStore({ + portalConfiguration: { + enterpriseId: 'test-enterprise-id', + enterpriseSlug: 'test-enterprise-slug', + enableLearnerPortal: false, + }, + }); + const props = { + isLoading: true, + tableData: { + results: [], + }, + }; + render(); + expect(screen.getByText('loading')).toBeTruthy(); + }); }); diff --git a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx index 09d2f29192..701e12dbb3 100644 --- a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx +++ b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx @@ -66,4 +66,16 @@ describe('', () => { render(); expect(screen.getByText('Budgets')); }); + it('Shows loading spinner', () => { + const enterpriseSubsidiesContextValue = { + ...defaultEnterpriseSubsidiesContextValue, + isLoading: true, + }; + render(); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); }); diff --git a/src/eventTracking.js b/src/eventTracking.js index f126e15c6e..a2f04f1cbc 100644 --- a/src/eventTracking.js +++ b/src/eventTracking.js @@ -18,12 +18,22 @@ const SETTINGS_PREFIX = `${PROJECT_NAME}.settings`; const CONTENT_HIGHLIGHTS_PREFIX = `${PROJECT_NAME}.content_highlights`; const LEARNER_CREDIT_MANAGEMENT_PREFIX = `${PROJECT_NAME}.learner_credit_management`; +// Sub-prefixes +// Subscriptions const SUBSCRIPTION_TABLE_PREFIX = `${SUBSCRIPTION_PREFIX}.table`; + +// ContentHighlights const CONTENT_HIGHLIGHT_STEPPER_BASE_PREFIX = `${CONTENT_HIGHLIGHTS_PREFIX}.stepper`; const CONTENT_HIGHLIGHTS_STEPPER_STEP_PREFIX = `${CONTENT_HIGHLIGHT_STEPPER_BASE_PREFIX}_step`; const CONTENT_HIGHLIGHTS_DASHBOARD_PREFIX = `${CONTENT_HIGHLIGHTS_PREFIX}.dashboard`; const CONTENT_HIGHLIGHTS_DELETE_CONTENT_PREFIX = `${CONTENT_HIGHLIGHTS_PREFIX}.delete_content_highlight`; +// learner-credit-management +const BUDGET_DETAIL_ACTIVITY_TAB_PREFIX = `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.activity`; +const BUDGET_DETAIL_CATALOG_TAB_PREFIX = `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.catalog`; +const BUDGET_DETAIL_SEARCH_PREFIX = `${BUDGET_DETAIL_CATALOG_TAB_PREFIX}.search`; +const BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX = `${BUDGET_DETAIL_CATALOG_TAB_PREFIX}.assignment_modal`; + export const SUBSCRIPTION_TABLE_EVENTS = { // Pagination PAGINATION_NEXT: `${SUBSCRIPTION_TABLE_PREFIX}.pagination.next.clicked`, @@ -75,7 +85,7 @@ export const CONTENT_HIGHLIGHTS_EVENTS = { HIGHLIGHT_DASHBOARD_SET_CATALOG_VISIBILITY: `${CONTENT_HIGHLIGHTS_DASHBOARD_PREFIX}.set_catalog_visibility.clicked`, HIGHLIGHT_DASHBOARD_SELECT_TAB: `${CONTENT_HIGHLIGHTS_DASHBOARD_PREFIX}.tab.clicked`, // Highlight Creation - NEW_HIGHLIHT_MAX_REACHED: `${CONTENT_HIGHLIGHTS_DASHBOARD_PREFIX}.create_new_content_highlight.max_reached.clicked`, + NEW_HIGHLIGHT_MAX_REACHED: `${CONTENT_HIGHLIGHTS_DASHBOARD_PREFIX}.create_new_content_highlight.max_reached.clicked`, NEW_HIGHLIGHT: `${CONTENT_HIGHLIGHTS_DASHBOARD_PREFIX}.create_new_content_highlight.clicked`, }; @@ -93,7 +103,27 @@ export const SUBSCRIPTION_EVENTS = { }; export const LEARNER_CREDIT_MANAGEMENT_EVENTS = { - TAB_CHANGED: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget-detail.tab.changed`, + TAB_CHANGED: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.tab.changed`, + // Activity tab + BUDGET_DETAILS_ASSIGNED_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table.changed`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_ACTIONS_REFRESH: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_refresh.clicked`, + BUDGET_DETAILS_SPENT_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.spent_table.changed`, + EMPTY_STATE_CTA: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.empty_state_cta_to_catalog.clicked`, + // Catalog tab + // Catalog tab search + VIEW_COURSE: `${BUDGET_DETAIL_SEARCH_PREFIX}.view_course.clicked`, + ASSIGN_COURSE: `${BUDGET_DETAIL_SEARCH_PREFIX}.assign_course_cta.clicked`, + // Catalog tab - Assignment Modal + TOGGLE_NEXT_STEPS: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.next_steps_collapsible.toggled`, + TOGGLE_IMPACT_ON_YOUR_LEARNERS: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.impact_on_your_learners_collapsible.toggled`, + TOGGLE_MANAGING_THIS_ASSIGNMENT: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.managing_this_assignment_collapsible.toggled`, + ASSIGNMENT_MODAL_CANCEL: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.close_modal_cancel.clicked`, + ASSIGNMENT_MODAL_EXIT: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.close_modal_exit.clicked`, + ASSIGNMENT_MODAL_ASSIGNMENT_ALLOCATION_ERROR: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.exit_assignment_allocation_modal.clicked`, + ASSIGNMENT_MODAL_HELP_CENTER: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.help_center_article_course_assignments.clicked`, + ASSIGNMENT_ALLOCATION_LEARNER_ASSIGNMENT: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.assignment_allocation.assigned`, + EMAIL_ADDRESS_VALIDATION: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.email_validation.changed`, + ASSIGNMENT_ALLOCATION_ERROR: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.assignment_allocation.errored`, }; const EVENT_NAMES = { From 10811a7ac559b4d072d2c629c5c9e99716c8b84f Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Wed, 29 Nov 2023 10:17:25 -0500 Subject: [PATCH 080/124] fix: update help text copy above budget detail tables to reflect Figma (#1105) --- src/components/App/index.jsx | 4 +- .../BudgetDetailRedemptions.jsx | 9 ++++- .../tests/BudgetDetailPage.test.jsx | 37 ++++++++++++++++++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/components/App/index.jsx b/src/components/App/index.jsx index ee1afbbf11..e56484d078 100644 --- a/src/components/App/index.jsx +++ b/src/components/App/index.jsx @@ -29,12 +29,12 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: defaultQueryClientRetryHandler, - // Specifying a longer `staleTime` of 20 seconds means queries will not refetch their data + // Specifying a longer `staleTime` of 60 seconds means queries will not refetch their data // as often; mitigates making duplicate queries when within the `staleTime` window, instead // relying on the cached data until the `staleTime` window has exceeded. This may be modified // per-query, as needed, if certain queries expect to be more up-to-date than others. Allows // `useQuery` to be used as a state manager. - staleTime: 1000 * 20, + staleTime: 1000 * 60, }, }, }); diff --git a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx index 0ea2fb766a..a9e114744f 100644 --- a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx +++ b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx @@ -21,8 +21,13 @@ const BudgetDetailRedemptions = ({ enterpriseFeatures, enterpriseUUID }) => {

    Spent

    - Spent activity is driven by completed enrollments. Enrollment data is automatically updated every 12 hours. - Come back later to view more recent enrollments. + Spent activity is driven by completed enrollments. + {(enterpriseOfferId || (subsidyAccessPolicyId && !enterpriseFeatures.topDownAssignmentRealTimeLcm)) && ( + <> + Enrollment data is automatically updated every 12 hours. + Come back later to view more recent enrollments. + + )}

    ', () => { it.each([ { budgetId: mockEnterpriseOfferId, + isTopDownAssignmentEnabled: true, expectedUseOfferRedemptionsArgs: [enterpriseUUID, mockEnterpriseOfferId, null, true], }, + { + budgetId: mockEnterpriseOfferId, + isTopDownAssignmentEnabled: false, + expectedUseOfferRedemptionsArgs: [enterpriseUUID, mockEnterpriseOfferId, null, false], + }, { budgetId: mockSubsidyAccessPolicyUUID, + isTopDownAssignmentEnabled: true, expectedUseOfferRedemptionsArgs: [enterpriseUUID, null, mockSubsidyAccessPolicyUUID, true], }, + { + budgetId: mockSubsidyAccessPolicyUUID, + isTopDownAssignmentEnabled: false, + expectedUseOfferRedemptionsArgs: [enterpriseUUID, null, mockSubsidyAccessPolicyUUID, false], + }, ])('displays spend table in "Activity" tab with empty results (%s)', async ({ budgetId, + isTopDownAssignmentEnabled, expectedUseOfferRedemptionsArgs, }) => { useParams.mockReturnValue({ @@ -277,7 +290,17 @@ describe('', () => { offerRedemptions: mockEmptyOfferRedemptions, fetchOfferRedemptions: jest.fn(), }); - renderWithRouter(); + const storeState = { + ...initialStoreState, + portalConfiguration: { + ...initialStoreState.portalConfiguration, + enterpriseFeatures: { + ...initialStoreState.portalConfiguration.enterpriseFeatures, + topDownAssignmentRealTimeLcm: isTopDownAssignmentEnabled, + }, + }, + }; + renderWithRouter(); expect(useOfferRedemptions).toHaveBeenCalledTimes(1); expect(useOfferRedemptions).toHaveBeenCalledWith(...expectedUseOfferRedemptionsArgs); @@ -287,9 +310,19 @@ describe('', () => { // Catalog tab does NOT exist since the budget is not assignable expect(screen.queryByText('Catalog')).not.toBeInTheDocument(); - // Spent table is visible within Activity tab contents + // Spent table and messaging is visible within Activity tab contents const spentSection = within(screen.getByText('Spent').closest('section')); expect(spentSection.getByText('No results found')).toBeInTheDocument(); + expect(spentSection.getByText('Spent activity is driven by completed enrollments.', { exact: false })).toBeInTheDocument(); + const isSubsidyAccessPolicyWithAnalyicsApi = ( + budgetId === mockSubsidyAccessPolicyUUID && !isTopDownAssignmentEnabled + ); + if (budgetId === mockEnterpriseOfferId || isSubsidyAccessPolicyWithAnalyicsApi) { + // This copy is only present when the "Spent" table is backed by the + // analytics API (i.e., budget is an enterprise offer or a subsidy access + // policy with the LC2 feature flag disabled). + expect(spentSection.getByText('Enrollment data is automatically updated every 12 hours.', { exact: false })).toBeInTheDocument(); + } }); it('renders with assigned table empty state with spent table and catalog tab available for assignable budgets', async () => { From 629abf01581c7361f48f4e3876602656ddcbec74 Mon Sep 17 00:00:00 2001 From: edX requirements bot <49161187+edx-requirements-bot@users.noreply.github.com> Date: Wed, 29 Nov 2023 10:35:07 -0500 Subject: [PATCH 081/124] chore: update browserslist DB (#964) Co-authored-by: abdullahwaheed Co-authored-by: Adam Stankiewicz --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86dab080e7..2d6e45a74f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8439,9 +8439,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001495", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001495.tgz", - "integrity": "sha512-F6x5IEuigtUfU5ZMQK2jsy5JqUUlEFRVZq8bO2a+ysq5K7jD6PPc9YXZj78xDNS3uNchesp1Jw47YXEqr+Viyg==", + "version": "1.0.30001564", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", + "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", "funding": [ { "type": "opencollective", From 66bcc8524008ca9801790f26fb6578b1a205b35c Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Tue, 28 Nov 2023 19:31:39 +0000 Subject: [PATCH 082/124] fix: adding stepper opening functionality to no sso card button --- .../settings/SettingsSSOTab/NoSSOCard.jsx | 4 +++- src/components/settings/SettingsSSOTab/index.jsx | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/settings/SettingsSSOTab/NoSSOCard.jsx b/src/components/settings/SettingsSSOTab/NoSSOCard.jsx index 0cad49a922..a8189d2d23 100644 --- a/src/components/settings/SettingsSSOTab/NoSSOCard.jsx +++ b/src/components/settings/SettingsSSOTab/NoSSOCard.jsx @@ -6,10 +6,11 @@ import { Add } from '@edx/paragon/icons'; import cardImage from '../../../data/images/NoSSO.svg'; const NoSSOCard = ({ - setShowNoSSOCard, setShowNewSSOForm, + setShowNoSSOCard, setShowNewSSOForm, setIsStepperOpen, }) => { const onClick = () => { setShowNoSSOCard(false); + setIsStepperOpen(true); setShowNewSSOForm(true); }; @@ -38,6 +39,7 @@ const NoSSOCard = ({ NoSSOCard.propTypes = { setShowNoSSOCard: PropTypes.func.isRequired, setShowNewSSOForm: PropTypes.func.isRequired, + setIsStepperOpen: PropTypes.func.isRequired, }; export default NoSSOCard; diff --git a/src/components/settings/SettingsSSOTab/index.jsx b/src/components/settings/SettingsSSOTab/index.jsx index 52ac8817ec..4fddf1263a 100644 --- a/src/components/settings/SettingsSSOTab/index.jsx +++ b/src/components/settings/SettingsSSOTab/index.jsx @@ -135,7 +135,11 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { )} {/* Nothing found so guide user to creation/edit form */} {showNoSSOCard && ( - + )} {/* Since we found a selected providerConfig we know we are in editing mode and can safely render the create/edit form */} @@ -193,7 +197,15 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { /> )} {/* Nothing found so guide user to creation/edit form */} - {showNoSSOCard && } + {/* Because NoSSOCard is shared component between old and new sso steppers, setIsStepperOpen is a placeholder + for now. This whole tree is scheduled to be deleted soon */} + {showNoSSOCard && ( + { }} + /> + )} {/* Since we found a selected providerConfig we know we are in editing mode and can safely render the create/edit form */} {((existingConfigs?.length > 0 && providerConfig !== null) || showNewSSOForm) && ( From a193f0f3fd44af90596a8921a030f18ea03dfbdb Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Wed, 29 Nov 2023 18:29:36 -0500 Subject: [PATCH 083/124] fix: temporarily patch @edx/paragon to have a correct select all count in DataTables (#1104) --- docs/decisions/0007-patch-package.rst | 42 +++ package-lock.json | 266 +++++++++++++++++- package.json | 2 + patches/@edx+paragon+20.46.3.patch | 29 ++ .../tests/BudgetDetailPage.test.jsx | 42 ++- 5 files changed, 364 insertions(+), 17 deletions(-) create mode 100644 docs/decisions/0007-patch-package.rst create mode 100644 patches/@edx+paragon+20.46.3.patch diff --git a/docs/decisions/0007-patch-package.rst b/docs/decisions/0007-patch-package.rst new file mode 100644 index 0000000000..9f3527e1c8 --- /dev/null +++ b/docs/decisions/0007-patch-package.rst @@ -0,0 +1,42 @@ +7. Utilizing ``patch-package`` for temporary fixes in third-party dependencies +============================================================================== + +Status +****** + +Accepted (November 2023) + +Context +******* + +The ``frontend-app-admin-portal`` repository is currently blocked on upgrading to the latest version of ``@edx/paragon`` until the React, React Router, ``@edx/frontend-platform``, etc. packages are upgraded first. Given this, we cannot rely on upstream fixes to ``@edx/paragon`` for bugs or security issues. + +There is at least one opportunity identified where there is a bug within Paragon's ``DataTable`` component, where the "Select all X" label's count is inaccurate when one or more rows are selected, with or without filters applied, using the ``DataTable.ControlledSelect*`` sub-components. A separate bug issue was filed upstream to the Paragon repository, but given ``frontend-app-admin-portal`` is blocked on a Paragon upgrade, we can turn to ``patch-package`` to temporarily fix the issue locally within this repository itself. + +``patch-package`` +----------------- + +``patch-package`` is an NPM package that allows one to modify third-party dependencies' code within ``node_modules``, and persist the patch so it's applied for other developers and within CI (i.e., anytime ``npm install`` is executed). + +By creating a temporary, committed patch file for ``@edx/paragon``, we can resolve the aforementioned "Select all X" label's inaccurate count without needing an upstream fix or upgrading to the latest version of ``@edx/paragon``. + +Decisions +********* + +We will use ``patch-package`` to temporarily create a patch of Paragon's ``DataTable`` component to shows the correct number in the "Select all X" label count until we can upgrade to the latest version of Paragon containing a fix for this issue. + +We will keep the ``patch-package`` devDependency installed and running in the ``postinstall`` NPM script. However, we should only reach for ``patch-package`` when necessary, and should not use it as a crutch for not upgrading to the latest version of a dependency or making a contribution to the upstream third-party dependency (e.g., ``@edx/paragon``). + +Consequences +************ + +* The generated patch file is versioned to the currently installed version of the patched dependency. If the installed version of the patched dependency changes, the patch file's version may need to be updated as well by ensuring the local package changes still function as expected and then generating the patch file (i.e., ``npx patch-package @edx/paragon``). +* Because code changes made in a generated patch are applied after ``npm install``, the changes in the patch should apply to unit and integration tests within this repository (i.e., the patch changes should be testable). + + +Alternatives Considered +*********************** + +* Implement a fix upstream in ``@edx/paragon`` and then upgrade to the latest versions of React, React Router, ``@edx/frontend-platform``, etc. in order to unblock the upgrade to the latest Paragon version. This was rejected because it would take focus off of the critical path of feature release with an impending deadline. +* Replicate a large portion of the Paragon code (i.e., ``SelectionStatusComponent``) into ``frontend-app-admin-portal`` to temporarily remove the "Select all X" label from the ``DataTable``. This was rejected because the "Select all X" functionality is intended to be there to make it easier to work with bulk actions on the table. Without "Select all X" functionality, users would need to manually paginate and select through each individual page which is not a good user experience. +* Do nothing. This was rejected as the state of the world without a fix leaves a known bug fix that causes usability issues and user confusion. diff --git a/package-lock.json b/package-lock.json index 2d6e45a74f..fe96608470 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "frontend-app-admin-portal", "version": "0.1.0", + "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { "@babel/plugin-transform-runtime": "7.12.1", @@ -77,6 +78,7 @@ "identity-obj-proxy": "3.0.0", "jest-canvas-mock": "^2.4.0", "jest-localstorage-mock": "^2.4.22", + "patch-package": "8.0.0", "postcss": "8.4.24", "react-dev-utils": "11.0.4", "react-test-renderer": "16.13.1", @@ -6975,6 +6977,12 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -8388,12 +8396,13 @@ "integrity": "sha512-O0KwuHuJnbHUrghHi2kGp0SxnWSIBXTYt7M8WVhW0kbPRUNUKoE/Of6e1rRD6AAxmfxFunKnt90yEK09D+sc5g==" }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9546,6 +9555,19 @@ "node": ">=8" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -11657,6 +11679,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -12033,9 +12064,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -16220,11 +16254,35 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -16247,6 +16305,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -16277,6 +16344,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -17600,6 +17676,15 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -17784,6 +17869,145 @@ "node": ">=0.10.0" } }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "dev": true, + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/patch-package/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -20693,6 +20917,20 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -22152,6 +22390,18 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 9cca08b084..e41c5aa0dd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .tsx --ext .ts .", "precommit": "npm run lint", "prepublishOnly": "npm run build", + "postinstall": "patch-package", "install-theme": "npm install \"@edx/brand@${THEME}\" --no-save", "start": "fedx-scripts webpack-dev-server --progress", "start:with-theme": "THEME=npm:@edx/brand-edx.org@latest npm run install-theme && fedx-scripts webpack-dev-server --progress", @@ -102,6 +103,7 @@ "identity-obj-proxy": "3.0.0", "jest-canvas-mock": "^2.4.0", "jest-localstorage-mock": "^2.4.22", + "patch-package": "8.0.0", "postcss": "8.4.24", "react-dev-utils": "11.0.4", "react-test-renderer": "16.13.1", diff --git a/patches/@edx+paragon+20.46.3.patch b/patches/@edx+paragon+20.46.3.patch new file mode 100644 index 0000000000..aa7efa884f --- /dev/null +++ b/patches/@edx+paragon+20.46.3.patch @@ -0,0 +1,29 @@ +diff --git a/node_modules/@edx/paragon/dist/DataTable/selection/BaseSelectionStatus.js b/node_modules/@edx/paragon/dist/DataTable/selection/BaseSelectionStatus.js +index 00c4c4b..426ca4e 100644 +--- a/node_modules/@edx/paragon/dist/DataTable/selection/BaseSelectionStatus.js ++++ b/node_modules/@edx/paragon/dist/DataTable/selection/BaseSelectionStatus.js +@@ -20,11 +20,12 @@ function BaseSelectionStatus(_ref) { + itemCount, + filteredRows, + isPaginated, +- state ++ state, ++ controlledTableSelections, + } = useContext(DataTableContext); + const hasAppliedFilters = state?.filters?.length > 0; +- const isAllRowsSelected = numSelectedRows === itemCount; +- const filteredItems = filteredRows?.length || itemCount; ++ const isAllRowsSelected = controlledTableSelections?.isEntireTableSelected || numSelectedRows === itemCount; ++ const selectAllItemCount = itemCount; + const intlAllSelectedText = allSelectedText || /*#__PURE__*/React.createElement(FormattedMessage, { + id: "pgn.DataTable.BaseSelectionStatus.allSelectedText", + defaultMessage: "All {numSelectedRows} selected", +@@ -62,7 +63,7 @@ function BaseSelectionStatus(_ref) { + defaultMessage: "Select all {itemCount}", + description: "A label for select all button.", + values: { +- itemCount: filteredItems ++ itemCount: selectAllItemCount + } + })), numSelectedRows > 0 && /*#__PURE__*/React.createElement(Button, { + className: CLEAR_SELECTION_TEST_ID, diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index e8a17870fe..0982821ad7 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -10,6 +10,8 @@ import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { act } from 'react-dom/test-utils'; +import { v4 as uuidv4 } from 'uuid'; +import { faker } from '@faker-js/faker'; import BudgetDetailPage from '../BudgetDetailPage'; import { @@ -113,6 +115,11 @@ const mockLearnerContentAssignment = { actions: [mockSuccessfulLinkedLearnerAction, mockSuccessfulNotifiedAction], errorReason: null, }; +const createMockLearnerContentAssignment = () => ({ + ...mockLearnerContentAssignment, + uuid: uuidv4(), + learnerEmail: faker.internet.email(), +}); const mockEnrollmentTransactionReversal = { uuid: 'test-transaction-reversal-uuid', created: '2023-10-31', @@ -384,6 +391,7 @@ describe('', () => { }); it('renders with assigned table data and handles table refresh', () => { + const NUMBER_OF_ASSIGNMENTS_TO_GENERATE = 60; useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', @@ -395,18 +403,23 @@ describe('', () => { useBudgetDetailActivityOverview.mockReturnValue({ isLoading: false, data: { - contentAssignments: { count: 1 }, + contentAssignments: { count: NUMBER_OF_ASSIGNMENTS_TO_GENERATE }, spentTransactions: { count: 0 }, }, }); const mockFetchContentAssignments = jest.fn(); + // Max page size is 25 rows. Generate one assignment with a known learner email and the others with random emails. + const mockAssignmentsList = [ + mockLearnerContentAssignment, + ...Array.from({ length: PAGE_SIZE - 1 }, createMockLearnerContentAssignment), + ]; useBudgetContentAssignments.mockReturnValue({ isLoading: false, contentAssignments: { - count: 1, - results: [mockLearnerContentAssignment], - learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], - numPages: 1, + count: NUMBER_OF_ASSIGNMENTS_TO_GENERATE, + results: mockAssignmentsList, + learnerStateCounts: [{ learnerState: 'waiting', count: NUMBER_OF_ASSIGNMENTS_TO_GENERATE }], + numPages: Math.floor(NUMBER_OF_ASSIGNMENTS_TO_GENERATE / PAGE_SIZE), currentPage: 1, }, fetchContentAssignments: mockFetchContentAssignments, @@ -422,12 +435,23 @@ describe('', () => { const assignedSection = within(screen.getByText('Assigned').closest('section')); expect(assignedSection.queryByText('No results found')).not.toBeInTheDocument(); expect(assignedSection.getByText(mockLearnerEmail)).toBeInTheDocument(); - const viewCourseCTA = assignedSection.getByText(mockContentTitle, { selector: 'a' }); + const viewCourseCTA = assignedSection.queryAllByText(mockContentTitle, { selector: 'a' })[0]; expect(viewCourseCTA).toBeInTheDocument(); expect(viewCourseCTA.getAttribute('href')).toEqual(`${process.env.ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${mockCourseKey}`); - expect(assignedSection.getByText('-$199')).toBeInTheDocument(); - expect(assignedSection.getByText('Waiting for learner')).toBeInTheDocument(); - expect(assignedSection.getByText(`Assigned: ${formatDate('2023-10-27')}`)).toBeInTheDocument(); + expect(assignedSection.queryAllByText('-$199')).toHaveLength(PAGE_SIZE); + expect(assignedSection.queryAllByText('Waiting for learner')).toHaveLength(PAGE_SIZE); + expect(assignedSection.queryAllByText(`Assigned: ${formatDate('2023-10-27')}`)).toHaveLength(PAGE_SIZE); + + // Assert the "Select all X" label count is correct, after selecting a row. This verifies the + // temporary patch of Paragon is working as intended. If this test fails, it may mean Paragon + // was upgraded to a version that does not yet contain a fix for the underlying bug related to + // the incorrect "Select all X" count. + const selectAllCheckbox = assignedSection.queryAllByRole('checkbox')[0]; + userEvent.click(selectAllCheckbox); + expect(getButtonElement(`Select all ${NUMBER_OF_ASSIGNMENTS_TO_GENERATE}`, { screenOverride: assignedSection })).toBeInTheDocument(); + + // Unselect the checkbox the "Refresh" table action appears + userEvent.click(selectAllCheckbox); const expectedTableFetchDataArgs = { pageIndex: DEFAULT_PAGE, From 6367d7afa99e435b32db4adf1a0850384735346c Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 30 Nov 2023 09:35:47 -0500 Subject: [PATCH 084/124] feat: adds additional track event to assigned and spent table courses (#1106) * feat: adds additional track event to assigned table courses * feat: adds track event on spent datatable view course * chore: test for spent table track event call * chore: test for spent table track event call * feat: add additional metadata to assignment segment event * feat: add additional metadata to view course from spend and assigned table * chore: test cleanup * fix: PR feedback --- .../AssignmentDetailsTableCell.jsx | 24 +++++++++++-- .../SpendTableEnrollmentDetails.jsx | 17 ++++++++++ .../cards/CourseCardFooterActions.jsx | 2 +- .../cards/NewAssignmentModalButton.jsx | 34 ++++++++++++++----- .../cards/tests/CourseCard.test.jsx | 4 +++ .../tests/CatalogSearchResults.test.jsx | 4 +++ .../tests/BudgetDetailPage.test.jsx | 8 ++++- src/eventTracking.js | 2 ++ 8 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx index 00b20f2419..83c8a0f26a 100644 --- a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx @@ -2,12 +2,25 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Stack, Hyperlink } from '@edx/paragon'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { configuration } from '../../config'; import EmailAddressTableCell from './EmailAddressTableCell'; +import EVENT_NAMES from '../../eventTracking'; -const AssignmentDetailsTableCell = ({ row, enterpriseSlug }) => { +const AssignmentDetailsTableCell = ({ row, enterpriseSlug, enterpriseId }) => { const { ENTERPRISE_LEARNER_PORTAL_URL } = configuration; + const handleOnViewCourseClick = () => sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_DETAILS_ASSIGNED_DATATABLE_VIEW_COURSE, + { + courseKey: row.original.contentKey, + contentQuantityInCents: row.original.contentQuantity, + errorReason: row.original.errorReason, + learnerState: row.original.learnerState, + state: row.original.state, + }, + ); return ( { @@ -30,19 +44,25 @@ const AssignmentDetailsTableCell = ({ row, enterpriseSlug }) => { }; const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, enterpriseSlug: state.portalConfiguration.enterpriseSlug, }); AssignmentDetailsTableCell.propTypes = { row: PropTypes.shape({ original: PropTypes.shape({ - uuid: PropTypes.string.isRequired, + uuid: PropTypes.string, learnerEmail: PropTypes.string, contentKey: PropTypes.string.isRequired, contentTitle: PropTypes.string, + contentQuantity: PropTypes.number, + errorReason: PropTypes.string, + learnerState: PropTypes.string, + state: PropTypes.string, }).isRequired, }).isRequired, enterpriseSlug: PropTypes.string, + enterpriseId: PropTypes.string.isRequired, }; export default connect(mapStateToProps)(AssignmentDetailsTableCell); diff --git a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx index 8ec52fd7cb..e4347ca1a8 100644 --- a/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx +++ b/src/components/learner-credit-management/SpendTableEnrollmentDetails.jsx @@ -3,14 +3,17 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Stack, Hyperlink } from '@edx/paragon'; import { getConfig } from '@edx/frontend-platform'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import EmailAddressTableCell from './EmailAddressTableCell'; import { formatDate } from './data'; +import EVENT_NAMES from '../../eventTracking'; const SpendTableEnrollmentDetailsContents = ({ row, enableLearnerPortal, enterpriseSlug, + enterpriseId, }) => ( {row.original.reversal && ( @@ -29,6 +32,16 @@ const SpendTableEnrollmentDetailsContents = ({ { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_DETAILS_SPENT_DATATABLE_VIEW_COURSE, + { + courseKey: row.original.courseKey, + courseListPriceInCents: row.original.courseListPrice * 100, + }, + ); + }} target="_blank" isInline > @@ -43,8 +56,10 @@ const SpendTableEnrollmentDetailsContents = ({ const rowPropType = PropTypes.shape({ original: PropTypes.shape({ + uuid: PropTypes.string.isRequired, courseKey: PropTypes.string.isRequired, courseTitle: PropTypes.string, + courseListPrice: PropTypes.number, userEmail: PropTypes.string, enterpriseEnrollmentId: PropTypes.number, fulfillmentIdentifier: PropTypes.string, @@ -57,6 +72,7 @@ const rowPropType = PropTypes.shape({ const mapStateToProps = state => ({ enableLearnerPortal: state.portalConfiguration.enableLearnerPortal, enterpriseSlug: state.portalConfiguration.enterpriseSlug, + enterpriseId: state.portalConfiguration.enterpriseId, }); const ConnectedSpendTableEnrollmentDetailsContents = connect(mapStateToProps)(SpendTableEnrollmentDetailsContents); @@ -65,6 +81,7 @@ SpendTableEnrollmentDetailsContents.propTypes = { row: rowPropType, enableLearnerPortal: PropTypes.bool.isRequired, enterpriseSlug: PropTypes.string.isRequired, + enterpriseId: PropTypes.string.isRequired, }; const SpendTableEnrollmentDetails = ({ row }) => ; diff --git a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx index 3866d007ee..38ac15756b 100644 --- a/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx +++ b/src/components/learner-credit-management/cards/CourseCardFooterActions.jsx @@ -16,7 +16,7 @@ const CourseCardFooterActions = ({ enterpriseId, course }) => { sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.VIEW_COURSE, - { courseUUID: uuid }, + { courseUuid: uuid }, ); }; return [ diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 353af471e3..7f20e7e8aa 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -16,7 +16,7 @@ import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform/utils'; import { connect } from 'react-redux'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; -import { learnerCreditManagementQueryKeys, useBudgetId } from '../data'; +import { learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy } from '../data'; import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals'; import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; import EVENT_NAMES from '../../../eventTracking'; @@ -42,19 +42,33 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const [assignButtonState, setAssignButtonState] = useState('default'); const [createAssignmentsErrorReason, setCreateAssignmentsErrorReason] = useState(); const { displayToastForAssignmentAllocation } = useContext(BudgetDetailPageContext); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, + } = subsidyAccessPolicy; + const sharedEnterpriseTrackEventMetadata = { + subsidyAccessPolicyId, + catalogUuid, + subsidyUuid, + isSubsidyActive, + isAssignable, + contentPriceCents: course.normalizedMetadata.contentPrice * 100, + contentKey: course.key, + courseUuid: course.uuid, + assignmentConfigurationUuid: assignmentConfiguration.uuid, + }; const { mutate } = useAllocateContentAssignments(); - const pathToActivityTab = generatePath(routeMatch.path, { budgetId: subsidyAccessPolicyId, activeTabKey: 'activity' }); const handleOpenAssignmentModal = () => { open(); sendEnterpriseTrackEvent( enterpriseId, - EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_ASSIGN_COURSE, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGN_COURSE, { + ...sharedEnterpriseTrackEventMetadata, isOpen: !isOpen, - courseUUID: course.uuid, }, ); }; @@ -74,13 +88,15 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { setCanAllocateAssignments(canAllocate); }, []); - const onSuccessEnterpriseTrackEvents = ({ created, noChange, updated }) => { + const onSuccessEnterpriseTrackEvents = ({ + created, noChange, updated, + }) => { const trackEventMetadata = { + ...sharedEnterpriseTrackEventMetadata, totalAllocatedLearners: learnerEmails.length, created: created.length, noChange: noChange.length, updated: updated.length, - courseUUID: course.uuid, }; sendEnterpriseTrackEvent( enterpriseId, @@ -108,7 +124,9 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), }); handleCloseAssignmentModal(); - onSuccessEnterpriseTrackEvents({ created, noChange, updated }); + onSuccessEnterpriseTrackEvents({ + created, noChange, updated, + }); displayToastForAssignmentAllocation({ totalLearnersAssigned: learnerEmails.length }); history.push(pathToActivityTab); }, @@ -130,8 +148,8 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_ALLOCATION_ERROR, { + ...sharedEnterpriseTrackEventMetadata, totalAllocatedLearners: learnerEmails.length, - courseUUID: course.uuid, errorStatus: httpErrorStatus, errorReason, }, diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 38d4d5267a..42697fc761 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -95,6 +95,10 @@ const initialStoreState = { const mockSubsidyAccessPolicy = { uuid: 'test-subsidy-access-policy-uuid', displayName: 'Test Subsidy Access Policy', + assignmentConfiguration: { + uuid: 'test-assignment-configuration-uuid', + active: true, + }, aggregates: { spendAvailableUsd: 50000, }, diff --git a/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx index 60107dbfa5..d46d96dec2 100644 --- a/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx @@ -31,6 +31,10 @@ jest.mock('../../data', () => ({ data: { uuid: 'test-uuid', displayName: 'Test Budget', + assignmentConfiguration: { + uuid: 'test-assignment-configuration-uuid', + active: true, + }, aggregates: { spendAvailableUsd: 100, }, diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 0982821ad7..170b33b0ad 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -388,6 +388,9 @@ describe('', () => { const transactionRowWithReversal = within(spentSection.getByText(mockSecondLearnerEmail).closest('tr')); expect(transactionRowWithReversal.getByText(`Refunded on ${formatDate(mockEnrollmentTransactionReversal.created)}`)).toBeInTheDocument(); expect(transactionRowWithReversal.getByText(`+${formatPrice(mockEnrollmentTransaction.courseListPrice)}`)).toBeInTheDocument(); + + userEvent.click(spentSection.queryAllByText(mockContentTitle, { selector: 'a' })[0]); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); }); it('renders with assigned table data and handles table refresh', () => { @@ -466,9 +469,12 @@ describe('', () => { const refreshCTA = assignedSection.getByText('Refresh', { selector: 'button' }); expect(refreshCTA).toBeInTheDocument(); userEvent.click(refreshCTA); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); expect(mockFetchContentAssignments).toHaveBeenCalledTimes(2); // should be called again on refresh expect(mockFetchContentAssignments).toHaveBeenLastCalledWith(expect.objectContaining(expectedTableFetchDataArgs)); - expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + + userEvent.click(viewCourseCTA); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); it.each([ diff --git a/src/eventTracking.js b/src/eventTracking.js index a2f04f1cbc..ed4244786f 100644 --- a/src/eventTracking.js +++ b/src/eventTracking.js @@ -106,8 +106,10 @@ export const LEARNER_CREDIT_MANAGEMENT_EVENTS = { TAB_CHANGED: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.tab.changed`, // Activity tab BUDGET_DETAILS_ASSIGNED_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table.changed`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_VIEW_COURSE: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_view_course.clicked`, BUDGET_DETAILS_ASSIGNED_DATATABLE_ACTIONS_REFRESH: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_refresh.clicked`, BUDGET_DETAILS_SPENT_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.spent_table.changed`, + BUDGET_DETAILS_SPENT_DATATABLE_VIEW_COURSE: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.spent_table_view_course.clicked`, EMPTY_STATE_CTA: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.empty_state_cta_to_catalog.clicked`, // Catalog tab // Catalog tab search From 9b52b1b4cac7de99f352c060e51b39bc31cf62aa Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Thu, 30 Nov 2023 01:01:52 +0000 Subject: [PATCH 085/124] fix: cleaning up sso network error page --- src/components/settings/SettingsSSOTab/SsoErrorPage.jsx | 5 +++-- src/components/settings/settings.scss | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/settings/SettingsSSOTab/SsoErrorPage.jsx b/src/components/settings/SettingsSSOTab/SsoErrorPage.jsx index 49450619d4..20849ffd44 100644 --- a/src/components/settings/SettingsSSOTab/SsoErrorPage.jsx +++ b/src/components/settings/SettingsSSOTab/SsoErrorPage.jsx @@ -19,17 +19,18 @@ const SsoErrorPage = ({ return ( { /* FullscreenModal requires an onClose prop despite hasCloseButton is false */ }} title={( edX logo title )} > - + Date: Fri, 1 Dec 2023 07:51:48 -0800 Subject: [PATCH 086/124] fix: Add loading spinner to Create new SSO button (#1107) test: Create new SSO button --- .../settings/SettingsSSOTab/index.jsx | 15 +++++-- .../tests/SettingsSSOTab.test.jsx | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/components/settings/SettingsSSOTab/index.jsx b/src/components/settings/SettingsSSOTab/index.jsx index 4fddf1263a..e98c94840b 100644 --- a/src/components/settings/SettingsSSOTab/index.jsx +++ b/src/components/settings/SettingsSSOTab/index.jsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { - Alert, ActionRow, Button, Hyperlink, ModalDialog, Toast, Skeleton, useToggle, + Alert, ActionRow, Button, Hyperlink, ModalDialog, Toast, Skeleton, Spinner, useToggle, } from '@edx/paragon'; import { Add, WarningFilled } from '@edx/paragon/icons'; import { HELP_CENTER_SAML_LINK } from '../data/constants'; @@ -30,12 +30,15 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { const [isOpen, open, close] = useToggle(false); const [pollingNetworkError, setPollingNetworkError] = useState(false); const [isStepperOpen, setIsStepperOpen] = useState(true); + const [isDeletingOldConfigs, setIsDeletingOldConfigs] = useState(false); const newConfigurationButtonOnClick = async () => { + setIsDeletingOldConfigs(true); Promise.all(existingConfigs.map(config => LmsApiService.updateEnterpriseSsoOrchestrationRecord( { active: false, is_removed: true }, config.uuid, ))).then(() => { + setIsDeletingOldConfigs(false); setRefreshBool(!refreshBool); close(); }); @@ -89,8 +92,13 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { @@ -151,7 +159,8 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { )} {pdError && ( - An error occurred loading the SAML data:

    {pdError?.message}

    + An error occurred loading the SAML data:{' '} +

    {pdError?.message}

    )} { ), ).toBeInTheDocument()); }); + test('creating new sso config with existing config', async () => { + features.AUTH0_SELF_SERVICE_INTEGRATION = true; + const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords'); + spy.mockImplementation(() => Promise.resolve({ + data: [{ + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + display_name: 'foobar', + active: true, + modified: '2022-04-12T19:51:25Z', + configured_at: '2022-05-12T19:51:25Z', + validated_at: '2022-06-12T19:51:25Z', + submitted_at: '2022-04-12T19:51:25Z', + }], + })); + const updateEnterpriseSsoOrchestrationRecord = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord'); + await waitFor(() => render( + + + + + , + + , + )); + await waitFor(() => expect( + screen.queryByText( + 'New', + ), + ).toBeInTheDocument()); + userEvent.click(screen.getByText('New')); + await waitFor(() => expect( + screen.queryByText( + 'Create new SSO configuration?', + ), + ).toBeInTheDocument()); + userEvent.click(screen.getByText('Create new SSO')); + expect(updateEnterpriseSsoOrchestrationRecord).toBeCalled(); + }); }); From d983dc7d757e94a5116fafe27ba6a35a57558b72 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 1 Dec 2023 11:16:07 -0500 Subject: [PATCH 087/124] feat: integrate with policies REST api on budgets overview page (#1109) --- .../EnterpriseAppContextProvider.jsx | 5 + .../EnterpriseAppContextProvider.test.jsx | 5 + .../EnterpriseApp/data/constants.js | 1 + src/components/EnterpriseApp/index.jsx | 6 + .../EnterpriseSubsidiesContext/data/hooks.js | 176 +++--- .../data/tests/hooks.test.js | 330 ------------ .../data/tests/hooks.test.jsx | 503 ++++++++++++++++++ .../data/tests/index.test.js | 43 +- .../EnterpriseSubsidiesContext/index.jsx | 35 +- .../learner-credit-management/BudgetCard.jsx | 96 ++-- .../BudgetDetailRedemptions.jsx | 12 +- .../MultipleBudgetsPage.jsx | 18 +- .../MultipleBudgetsPicker.jsx | 17 +- .../SubBudgetCard.jsx | 35 +- .../cards/NewAssignmentModalButton.jsx | 7 + .../cards/tests/CourseCard.test.jsx | 5 +- .../data/constants.js | 2 +- .../data/hooks/index.js | 4 +- ...test.jsx => useBudgetRedemptions.test.jsx} | 16 +- .../data/hooks/tests/useOfferSummary.test.js | 67 --- .../useSubsidySummaryAnalyticsApi.test.js | 93 ++++ ...Redemptions.js => useBudgetRedemptions.js} | 19 +- .../data/hooks/useOfferSummary.js | 41 -- .../hooks/useSubsidySummaryAnalyticsApi.js | 47 ++ .../data/tests/utils.test.js | 54 +- .../learner-credit-management/data/utils.js | 60 ++- .../learner-credit-management/index.jsx | 4 +- .../tests/BudgetCard.test.jsx | 346 ++++++------ .../tests/BudgetDetailPage.test.jsx | 76 +-- .../tests/MultipleBudgetsPage.test.jsx | 6 +- .../tests/SubsidyRequestsContext.test.jsx | 2 +- src/containers/EnterpriseApp/index.jsx | 1 + src/data/constants/subsidyTypes.js | 2 +- .../services/EnterpriseAccessApiService.js | 16 + .../tests/EnterpriseAccessApiService.test.js | 7 + 35 files changed, 1269 insertions(+), 888 deletions(-) delete mode 100644 src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js create mode 100644 src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx rename src/components/learner-credit-management/data/hooks/tests/{useOfferRedemptions.test.jsx => useBudgetRedemptions.test.jsx} (91%) delete mode 100644 src/components/learner-credit-management/data/hooks/tests/useOfferSummary.test.js create mode 100644 src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js rename src/components/learner-credit-management/data/hooks/{useOfferRedemptions.js => useBudgetRedemptions.js} (91%) delete mode 100644 src/components/learner-credit-management/data/hooks/useOfferSummary.js create mode 100644 src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js diff --git a/src/components/EnterpriseApp/EnterpriseAppContextProvider.jsx b/src/components/EnterpriseApp/EnterpriseAppContextProvider.jsx index b197cf81fb..9d7960ae21 100644 --- a/src/components/EnterpriseApp/EnterpriseAppContextProvider.jsx +++ b/src/components/EnterpriseApp/EnterpriseAppContextProvider.jsx @@ -45,6 +45,7 @@ export const EnterpriseAppContext = createContext(); const EnterpriseAppContextProvider = ({ enterpriseId, enterpriseName, + enterpriseFeatures, enablePortalLearnerCreditManagementScreen, children, }) => { @@ -52,6 +53,7 @@ const EnterpriseAppContextProvider = ({ const enterpriseSubsidiesContext = useEnterpriseSubsidiesContext({ enterpriseId, enablePortalLearnerCreditManagementScreen, + isTopDownAssignmentEnabled: enterpriseFeatures.topDownAssignmentRealTimeLcm, }); // subsidy requests for the enterprise customer @@ -97,6 +99,9 @@ const EnterpriseAppContextProvider = ({ EnterpriseAppContextProvider.propTypes = { enterpriseId: PropTypes.string.isRequired, enterpriseName: PropTypes.string.isRequired, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool, + }).isRequired, enablePortalLearnerCreditManagementScreen: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, }; diff --git a/src/components/EnterpriseApp/EnterpriseAppContextProvider.test.jsx b/src/components/EnterpriseApp/EnterpriseAppContextProvider.test.jsx index 5d490ebcd0..892200045b 100644 --- a/src/components/EnterpriseApp/EnterpriseAppContextProvider.test.jsx +++ b/src/components/EnterpriseApp/EnterpriseAppContextProvider.test.jsx @@ -14,6 +14,10 @@ const TEST_ENTERPRISE_NAME = 'test-enterprise-name'; jest.mock('./data/hooks'); +const mockEnterpriseFeatures = { + topDownAssignmentRealTimeLcm: true, +}; + describe('', () => { it.each([{ isLoadingEnterpriseSubsidies: true, @@ -63,6 +67,7 @@ describe('', () => { children diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js index 1773fd73e2..e37edb3636 100644 --- a/src/components/EnterpriseApp/data/constants.js +++ b/src/components/EnterpriseApp/data/constants.js @@ -23,4 +23,5 @@ export const BUDGET_STATUSES = { export const BUDGET_TYPES = { ecommerce: 'ecommerce', subsidy: 'subsidy', + policy: 'policy', }; diff --git a/src/components/EnterpriseApp/index.jsx b/src/components/EnterpriseApp/index.jsx index c05a732308..13617549f4 100644 --- a/src/components/EnterpriseApp/index.jsx +++ b/src/components/EnterpriseApp/index.jsx @@ -86,6 +86,7 @@ class EnterpriseApp extends React.Component { enablePortalLearnerCreditManagementScreen, enterpriseId, enterpriseName, + enterpriseFeatures, enterpriseBranding, loading, } = this.props; @@ -123,6 +124,7 @@ class EnterpriseApp extends React.Component { @@ -173,6 +175,7 @@ class EnterpriseApp extends React.Component { EnterpriseApp.defaultProps = { enterpriseId: null, enterpriseName: null, + enterpriseFeatures: {}, enterpriseBranding: { primary_color: SCHOLAR_THEME.button, secondary_color: SCHOLAR_THEME.banner, @@ -196,6 +199,9 @@ EnterpriseApp.propTypes = { }).isRequired, enterpriseId: PropTypes.string, enterpriseName: PropTypes.string, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool, + }), enterpriseBranding: PropTypes.shape({ primary_color: PropTypes.string, secondary_color: PropTypes.string, diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index 6e9b040167..c576a19541 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -1,87 +1,121 @@ import { useEffect, useState } from 'react'; import dayjs from 'dayjs'; -import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; -import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; - +import isBetween from 'dayjs/plugin/isBetween'; import { logError } from '@edx/frontend-platform/logging'; -import { getConfig } from '@edx/frontend-platform/config'; import { camelCaseObject } from '@edx/frontend-platform/utils'; +import { getConfig } from '@edx/frontend-platform/config'; +import { useQuery } from '@tanstack/react-query'; import EcommerceApiService from '../../../data/services/EcommerceApiService'; import LicenseManagerApiService from '../../../data/services/LicenseManagerAPIService'; import SubsidyApiService from '../../../data/services/EnterpriseSubsidyApiService'; import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; - -export const useEnterpriseOffers = ({ enablePortalLearnerCreditManagementScreen, enterpriseId }) => { - const [offers, setOffers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [canManageLearnerCredit, setCanManageLearnerCredit] = useState(false); - - dayjs.extend(isSameOrBefore); - dayjs.extend(isSameOrAfter); - - useEffect(() => { - setIsLoading(true); - const fetchOffers = async () => { - try { - const [enterpriseSubsidyResponse, ecommerceApiResponse] = await Promise.all([ - SubsidyApiService.getSubsidyByCustomerUUID(enterpriseId, { subsidyType: 'learner_credit' }), - EcommerceApiService.fetchEnterpriseOffers(), - ]); - - // We have to consider both type of offers active and inactive. - - const enterpriseSubsidyResults = camelCaseObject(enterpriseSubsidyResponse.data).results; - const ecommerceOffersResults = camelCaseObject(ecommerceApiResponse.data.results); - - const offerData = []; - - enterpriseSubsidyResults.forEach((result) => { - offerData.push({ - source: BUDGET_TYPES.subsidy, - id: result.uuid, - name: result.title, - start: result.activeDatetime, - end: result.expirationDatetime, - isCurrent: result.isActive, - }); - }); - - ecommerceOffersResults.forEach((result) => { - offerData.push({ - source: BUDGET_TYPES.ecommerce, - id: (result.id).toString(), - name: result.displayName, - start: result.startDatetime, - end: result.endDatetime, - isCurrent: result.isCurrent, - }); - }); - setOffers(offerData); - if (offerData.length > 0) { - setCanManageLearnerCredit(true); - } - } catch (error) { - logError(error); - } finally { - setIsLoading(false); - } +import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; +import { learnerCreditManagementQueryKeys } from '../../learner-credit-management/data'; + +dayjs.extend(isBetween); + +async function fetchEnterpriseBudgets({ + isTopDownAssignmentEnabled, + enterpriseId, + enablePortalLearnerCreditManagementScreen, +}) { + // If the LC2 feature is disabled, do nothing. + if (!getConfig().FEATURE_LEARNER_CREDIT_MANAGEMENT || !enablePortalLearnerCreditManagementScreen) { + return { + budgets: [], + canManageLearnerCredit: false, }; - - if (getConfig().FEATURE_LEARNER_CREDIT_MANAGEMENT - && enablePortalLearnerCreditManagementScreen) { - fetchOffers(); - } else { - setIsLoading(false); - } - }, [enablePortalLearnerCreditManagementScreen, enterpriseId]); + } + + // Call the appropriate API based on the feature flag + const budgetPromisesToFulfill = isTopDownAssignmentEnabled + ? [undefined, EnterpriseAccessApiService.listSubsidyAccessPolicies(enterpriseId)] + : [SubsidyApiService.getSubsidyByCustomerUUID(enterpriseId, { subsidyType: 'learner_credit' }), undefined]; + + // Always prepend the promise to fetch ecommerce offers + budgetPromisesToFulfill.unshift(EcommerceApiService.fetchEnterpriseOffers()); + + // Attempt to resolve all promises + const [ + ecommerceApiResponse, + enterpriseSubsidyResponse, + enterprisePolicyResponse, + ] = await Promise.allSettled(budgetPromisesToFulfill); + + // Log any errors + if (ecommerceApiResponse.status === 'rejected') { + logError(ecommerceApiResponse.reason); + } + if (enterpriseSubsidyResponse.status === 'rejected') { + logError(enterpriseSubsidyResponse.reason); + } + if (enterprisePolicyResponse.status === 'rejected') { + logError(enterprisePolicyResponse.reason); + } + + // Transform the API responses + const ecommerceOffersResults = camelCaseObject(ecommerceApiResponse.value?.data.results); + const enterpriseSubsidyResults = camelCaseObject(enterpriseSubsidyResponse.value?.data.results); + const enterprisePolicyResults = camelCaseObject(enterprisePolicyResponse.value?.data.results); + + // Iterate through each API response (if applicable) and concatenate the results into a single array of budgets. + const budgetsList = []; + enterprisePolicyResults?.forEach((result) => { + budgetsList.push({ + source: BUDGET_TYPES.policy, + id: result.uuid, + name: result.displayName || 'Overview', + start: result.subsidyActiveDatetime, + end: result.subsidyExpirationDatetime, + isCurrent: dayjs().isBetween(result.subsidyActiveDatetime, result.subsidyExpirationDatetime, 'day', '[]'), + aggregates: { + available: result.aggregates.spendAvailableUsd, + spent: result.aggregates.amountRedeemedUsd, + pending: result.aggregates.amountAllocatedUsd, + }, + }); + }); + enterpriseSubsidyResults?.forEach((result) => { + budgetsList.push({ + source: BUDGET_TYPES.subsidy, + id: result.uuid, + name: result.title, + start: result.activeDatetime, + end: result.expirationDatetime, + isCurrent: result.isActive, + }); + }); + ecommerceOffersResults?.forEach((result) => { + budgetsList.push({ + source: BUDGET_TYPES.ecommerce, + id: (result.id).toString(), + name: result.displayName, + start: result.startDatetime, + end: result.endDatetime, + isCurrent: result.isCurrent, + }); + }); return { - isLoading, - offers, - canManageLearnerCredit, + budgets: budgetsList, + canManageLearnerCredit: budgetsList.length > 0, }; -}; +} + +export const useEnterpriseBudgets = ({ + enablePortalLearnerCreditManagementScreen, + enterpriseId, + isTopDownAssignmentEnabled, +}) => useQuery({ + queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseId), + queryFn: (args) => fetchEnterpriseBudgets({ + queryArgs: args, + isTopDownAssignmentEnabled, + enterpriseId, + enablePortalLearnerCreditManagementScreen, + }), +}); export const useCustomerAgreement = ({ enterpriseId }) => { const [customerAgreement, setCustomerAgreement] = useState(); diff --git a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js deleted file mode 100644 index ec1c5466af..0000000000 --- a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js +++ /dev/null @@ -1,330 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks/dom'; - -import { useCoupons, useCustomerAgreement, useEnterpriseOffers } from '../hooks'; -import EcommerceApiService from '../../../../data/services/EcommerceApiService'; -import LicenseManagerApiService from '../../../../data/services/LicenseManagerAPIService'; -import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; -import { BUDGET_TYPES } from '../../../EnterpriseApp/data/constants'; - -jest.mock('@edx/frontend-platform/config', () => ({ - getConfig: jest.fn(() => ({ - FEATURE_LEARNER_CREDIT_MANAGEMENT: true, - })), -})); -jest.mock('../../../../data/services/EcommerceApiService'); -jest.mock('../../../../data/services/LicenseManagerAPIService'); -jest.mock('../../../../data/services/EnterpriseAccessApiService'); -jest.mock('../../../../data/services/EnterpriseSubsidyApiService'); - -const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; - -describe('useEnterpriseOffers', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should not fetch enterprise offers if enablePortalLearnerCreditManagementScreen is false', async () => { - const { result } = renderHook(() => useEnterpriseOffers({ enablePortalLearnerCreditManagementScreen: false })); - - expect(EcommerceApiService.fetchEnterpriseOffers).not.toHaveBeenCalled(); - expect(SubsidyApiService.getSubsidyByCustomerUUID).not.toHaveBeenCalled(); - - expect(result.current).toEqual({ - offers: [], - isLoading: false, - canManageLearnerCredit: false, - }); - }); - - it('should fetch enterprise offers for the enterprise when data is available only in e-commerce', async () => { - const mockEcommerceResponse = [ - { - id: 'uuid', - display_name: 'offer-name', - start_datetime: '2021-05-15T19:56:09Z', - end_datetime: '2100-05-15T19:56:09Z', - is_current: true, - }, - ]; - const mockOffers = [{ - id: 'uuid', - name: 'offer-name', - start: '2021-05-15T19:56:09Z', - end: '2100-05-15T19:56:09Z', - isCurrent: true, - source: BUDGET_TYPES.ecommerce, - }]; - - SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ - data: { - results: [], - }, - }); - EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { - results: mockEcommerceResponse, - }, - }); - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers({ - enablePortalLearnerCreditManagementScreen: true, - })); - - await waitForNextUpdate(); - - expect(EcommerceApiService.fetchEnterpriseOffers).toHaveBeenCalled(); - expect(result.current).toEqual({ - offers: mockOffers, - isLoading: false, - canManageLearnerCredit: true, - }); - }); - - it('should fetch enterprise offers for the enterprise when data available in enterprise-subsidy', async () => { - const mockEnterpriseSubsidyResponse = [ - { - uuid: 'offer-id', - title: 'offer-name', - activeDatetime: '2021-05-15T19:56:09Z', - expirationDatetime: '2100-05-15T19:56:09Z', - isActive: true, - }, - ]; - - const mockEcommerceResponse = [ - { - id: 'uuid', - display_name: 'offer-name', - start_datetime: '2021-05-15T19:56:09Z', - end_datetime: '2100-05-15T19:56:09Z', - is_current: true, - }, - ]; - - SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ - data: { - results: mockEnterpriseSubsidyResponse, - }, - }); - - EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { - results: mockEcommerceResponse, - }, - }); - - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers({ - enablePortalLearnerCreditManagementScreen: true, - enterpriseId: TEST_ENTERPRISE_UUID, - })); - - await waitForNextUpdate(); - - expect(SubsidyApiService.getSubsidyByCustomerUUID).toHaveBeenCalledWith( - TEST_ENTERPRISE_UUID, - { subsidyType: 'learner_credit' }, - ); - - const expectedOffers = [ - { - id: 'offer-id', - name: 'offer-name', - start: '2021-05-15T19:56:09Z', - end: '2100-05-15T19:56:09Z', - isCurrent: true, - source: BUDGET_TYPES.subsidy, - }, - { - id: 'uuid', - name: 'offer-name', - start: '2021-05-15T19:56:09Z', - end: '2100-05-15T19:56:09Z', - isCurrent: true, - source: BUDGET_TYPES.ecommerce, - }, - ]; - - expect(result.current).toEqual({ - offers: expectedOffers, - isLoading: false, - canManageLearnerCredit: true, - }); - }); - - it('should set canManageLearnerCredit to false if active enterprise offer or subsidy not found', async () => { - const mockSubsidyServiceResponse = []; - - EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { - results: [], - }, - }); - SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ - data: { - results: mockSubsidyServiceResponse, - }, - }); - - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers({ - enablePortalLearnerCreditManagementScreen: true, - enterpriseId: TEST_ENTERPRISE_UUID, - })); - - await waitForNextUpdate(); - - expect(SubsidyApiService.getSubsidyByCustomerUUID).toHaveBeenCalledWith( - TEST_ENTERPRISE_UUID, - { subsidyType: 'learner_credit' }, - ); - - const hasActiveOffersOrSubsidies = mockSubsidyServiceResponse.some(offer => offer.is_active); - let canManageLearnerCredit = false; - - if (hasActiveOffersOrSubsidies) { - canManageLearnerCredit = true; - } - - expect(result.current).toEqual({ - offers: [], - isLoading: false, - canManageLearnerCredit, - }); - }); - - it('should return the active enterprise offer or subsidy when multiple available', async () => { - const mockSubsidyServiceResponse = [ - { - uuid: 'offer-1', - title: 'offer-name', - active_datetime: '2005-05-15T19:56:09Z', - expiration_datetime: '2006-05-15T19:56:09Z', - is_active: false, - }, - { - uuid: 'offer-2', - title: 'offer-name-2', - active_datetime: '2006-05-15T19:56:09Z', - expiration_datetime: '2099-05-15T19:56:09Z', - is_active: true, - }, - ]; - const mockOfferData = [ - { - id: 'offer-1', - name: 'offer-name', - start: '2005-05-15T19:56:09Z', - end: '2006-05-15T19:56:09Z', - isCurrent: false, - source: BUDGET_TYPES.subsidy, - }, - { - id: 'offer-2', - name: 'offer-name-2', - start: '2006-05-15T19:56:09Z', - end: '2099-05-15T19:56:09Z', - isCurrent: true, - source: BUDGET_TYPES.subsidy, - }, - ]; - - EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { - results: [], - }, - }); - SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ - data: { - results: mockSubsidyServiceResponse, - }, - }); - - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers({ - enablePortalLearnerCreditManagementScreen: true, - enterpriseId: TEST_ENTERPRISE_UUID, - })); - - await waitForNextUpdate(); - - expect(SubsidyApiService.getSubsidyByCustomerUUID).toHaveBeenCalledWith( - TEST_ENTERPRISE_UUID, - { subsidyType: 'learner_credit' }, - ); - expect(result.current).toEqual({ - offers: mockOfferData, - isLoading: false, - canManageLearnerCredit: true, - }); - }); -}); - -describe('useCustomerAgreement', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should fetch customer agreement for the enterprise', async () => { - const mockCustomerAgreement = { - subscriptions: [], - }; - LicenseManagerApiService.fetchCustomerAgreementData.mockResolvedValueOnce({ - data: { - results: [mockCustomerAgreement], - }, - }); - const { result, waitForNextUpdate } = renderHook(() => useCustomerAgreement({ - enterpriseId: TEST_ENTERPRISE_UUID, - })); - - await waitForNextUpdate(); - - expect(LicenseManagerApiService.fetchCustomerAgreementData).toHaveBeenCalledWith({ - enterprise_customer_uuid: TEST_ENTERPRISE_UUID, - }); - expect(result.current).toEqual({ - customerAgreement: mockCustomerAgreement, - isLoading: false, - }); - }); - - it('should should not set customer agreement if results are empty', async () => { - LicenseManagerApiService.fetchCustomerAgreementData.mockResolvedValueOnce({ - data: { - results: [], - }, - }); - const { result, waitForNextUpdate } = renderHook(() => useCustomerAgreement({ - enterpriseId: TEST_ENTERPRISE_UUID, - })); - - await waitForNextUpdate(); - - expect(LicenseManagerApiService.fetchCustomerAgreementData).toHaveBeenCalled(); - expect(result.current).toEqual({ - customerAgreement: undefined, - isLoading: false, - }); - }); -}); - -describe('useCoupons', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should fetch coupons for the enterprise', async () => { - const mockCoupons = [{ uuid: 'test-coupon' }]; - EcommerceApiService.fetchCouponOrders.mockResolvedValueOnce({ - data: { - results: mockCoupons, - }, - }); - const { result, waitForNextUpdate } = renderHook(() => useCoupons()); - - await waitForNextUpdate(); - - expect(EcommerceApiService.fetchCouponOrders).toHaveBeenCalled(); - expect(result.current).toEqual({ - coupons: mockCoupons, - isLoading: false, - }); - }); -}); diff --git a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx new file mode 100644 index 0000000000..4a8b09fa08 --- /dev/null +++ b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx @@ -0,0 +1,503 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; +import { logError } from '@edx/frontend-platform/logging'; +import dayjs from 'dayjs'; +import { QueryClientProvider } from '@tanstack/react-query'; + +import { useCoupons, useCustomerAgreement, useEnterpriseBudgets } from '../hooks'; +import EcommerceApiService from '../../../../data/services/EcommerceApiService'; +import LicenseManagerApiService from '../../../../data/services/LicenseManagerAPIService'; +import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService'; +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import { BUDGET_TYPES } from '../../../EnterpriseApp/data/constants'; +import { queryClient } from '../../../test/testUtils'; + +jest.mock('@edx/frontend-platform/config', () => ({ + ...jest.requireActual('@edx/frontend-platform/config'), + getConfig: jest.fn(() => ({ + FEATURE_LEARNER_CREDIT_MANAGEMENT: true, + })), +})); +jest.mock('@edx/frontend-platform/logging', () => ({ + ...jest.requireActual('@edx/frontend-platform/logging'), + logError: jest.fn(), +})); +jest.mock('../../../../data/services/EcommerceApiService'); +jest.mock('../../../../data/services/LicenseManagerAPIService'); +jest.mock('../../../../data/services/EnterpriseAccessApiService'); +jest.mock('../../../../data/services/EnterpriseSubsidyApiService'); + +const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; + +describe('useEnterpriseBudgets', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const fetchEnterpriseOffersSpy = jest.spyOn(EcommerceApiService, 'fetchEnterpriseOffers').mockResolvedValue({ + data: { + results: [], + }, + }); + const getSubsidyByCustomerUUIDSpy = jest.spyOn(SubsidyApiService, 'getSubsidyByCustomerUUID').mockResolvedValue({ + data: { + results: [], + }, + }); + const listSubsidyAccessPoliciesSpy = jest.spyOn(EnterpriseAccessApiService, 'listSubsidyAccessPolicies').mockResolvedValue({ + data: { + results: [], + }, + }); + + const mockBudgetStart = dayjs().subtract(1, 'week').toISOString(); + const mockBudgetEnd = dayjs().add(1, 'week').toISOString(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should not fetch any budgets if enablePortalLearnerCreditManagementScreen is false', async () => { + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen: false, + isTopDownAssignmentEnabled: false, + enterpriseId: TEST_ENTERPRISE_UUID, + }), + { wrapper }, + ); + + expect(EcommerceApiService.fetchEnterpriseOffers).not.toHaveBeenCalled(); + expect(SubsidyApiService.getSubsidyByCustomerUUID).not.toHaveBeenCalled(); + expect(EnterpriseAccessApiService.listSubsidyAccessPolicies).not.toHaveBeenCalled(); + + await waitForNextUpdate(); + + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + canManageLearnerCredit: false, + budgets: [], + }, + }), + ); + }); + + it('should always fetch enterprise offers from ecommerce', async () => { + const mockEcommerceResponse = [ + { + id: 'uuid', + display_name: 'offer-name', + start_datetime: mockBudgetStart, + end_datetime: mockBudgetEnd, + is_current: true, + }, + ]; + const mockBudgets = [{ + id: 'uuid', + name: 'offer-name', + start: mockBudgetStart, + end: mockBudgetEnd, + isCurrent: true, + source: BUDGET_TYPES.ecommerce, + }]; + fetchEnterpriseOffersSpy.mockResolvedValue({ + data: { + results: mockEcommerceResponse, + }, + }); + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen: true, + }), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(fetchEnterpriseOffersSpy).toHaveBeenCalledTimes(1); + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + budgets: mockBudgets, + canManageLearnerCredit: true, + }, + }), + ); + }); + + it('should fetch Subsidy-associated budgets from enterprise-subsidy when LC2 feature flag is disabled', async () => { + const mockEnterpriseSubsidyResponse = [ + { + uuid: 'offer-id', + title: 'offer-name', + activeDatetime: mockBudgetStart, + expirationDatetime: mockBudgetEnd, + isActive: true, + }, + ]; + getSubsidyByCustomerUUIDSpy.mockResolvedValue({ + data: { + results: mockEnterpriseSubsidyResponse, + }, + }); + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen: true, + enterpriseId: TEST_ENTERPRISE_UUID, + isTopDownAssignmentEnabled: false, + }), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(getSubsidyByCustomerUUIDSpy).toHaveBeenCalledTimes(1); + expect(getSubsidyByCustomerUUIDSpy).toHaveBeenCalledWith( + TEST_ENTERPRISE_UUID, + { subsidyType: 'learner_credit' }, + ); + expect(listSubsidyAccessPoliciesSpy).not.toHaveBeenCalled(); + + const expectedBudgets = [ + { + id: 'offer-id', + name: 'offer-name', + start: mockBudgetStart, + end: mockBudgetEnd, + isCurrent: true, + source: BUDGET_TYPES.subsidy, + }, + { + id: 'uuid', + name: 'offer-name', + start: mockBudgetStart, + end: mockBudgetEnd, + isCurrent: true, + source: BUDGET_TYPES.ecommerce, + }, + ]; + + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + budgets: expectedBudgets, + canManageLearnerCredit: true, + }, + }), + ); + }); + + it('should fetch Subsidy Access Policies (budgets) from enterprise-access when LC2 feature flag is enabled', async () => { + const mockEnterprisePoliciesResponse = [ + { + uuid: 'budget-uuid', + displayName: 'Budget Display name', + subsidyActiveDatetime: mockBudgetStart, + subsidyExpirationDatetime: mockBudgetEnd, + active: true, + aggregates: { + spendAvailableUsd: 700, + amountRedeemedUsd: 200, + amountAllocatedUsd: 100, + }, + }, + ]; + fetchEnterpriseOffersSpy.mockResolvedValue({ + data: { + results: [], + }, + }); + listSubsidyAccessPoliciesSpy.mockResolvedValue({ + data: { + results: mockEnterprisePoliciesResponse, + }, + }); + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen: true, + enterpriseId: TEST_ENTERPRISE_UUID, + isTopDownAssignmentEnabled: true, + }), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(listSubsidyAccessPoliciesSpy).toHaveBeenCalledTimes(1); + expect(listSubsidyAccessPoliciesSpy).toHaveBeenCalledWith(TEST_ENTERPRISE_UUID); + expect(getSubsidyByCustomerUUIDSpy).not.toHaveBeenCalled(); + + const expectedBudgets = [ + { + id: 'budget-uuid', + name: 'Budget Display name', + start: mockBudgetStart, + end: mockBudgetEnd, + isCurrent: true, + source: BUDGET_TYPES.policy, + aggregates: { + available: 700, + spent: 200, + pending: 100, + }, + }, + ]; + + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + budgets: expectedBudgets, + canManageLearnerCredit: true, + }, + }), + ); + }); + + it.each([ + { isTopDownAssignmentEnabled: false }, + { isTopDownAssignmentEnabled: true }, + ])('should log error when budgets API request cannot be fulfilled (%s)', async ({ isTopDownAssignmentEnabled }) => { + const mockListOffersError = 'error_list_offers'; + const mockListSubsidiesError = 'error_list_subsidies'; + const mockListPoliciesError = 'error_list_policies'; + + fetchEnterpriseOffersSpy.mockRejectedValue(mockListOffersError); + getSubsidyByCustomerUUIDSpy.mockRejectedValue(mockListSubsidiesError); + listSubsidyAccessPoliciesSpy.mockRejectedValue(mockListPoliciesError); + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen: true, + enterpriseId: TEST_ENTERPRISE_UUID, + isTopDownAssignmentEnabled, + }), + { wrapper }, + ); + + await waitForNextUpdate(); + + // Assert the failed enterprise offers API call + expect(logError).toHaveBeenCalledWith(mockListOffersError); + + if (isTopDownAssignmentEnabled) { + // Assert the failed API call to list subsidy access policies from enterprise-access + expect(listSubsidyAccessPoliciesSpy).toHaveBeenCalledTimes(1); + expect(listSubsidyAccessPoliciesSpy).toHaveBeenCalledWith(TEST_ENTERPRISE_UUID); + expect(getSubsidyByCustomerUUIDSpy).not.toHaveBeenCalled(); + + expect(logError).toHaveBeenCalledWith(mockListPoliciesError); + } else { + // Assert the failed API call to list subsidies from enterprise-subsidy + expect(getSubsidyByCustomerUUIDSpy).toHaveBeenCalledTimes(1); + expect(getSubsidyByCustomerUUIDSpy).toHaveBeenCalledWith( + TEST_ENTERPRISE_UUID, + { subsidyType: 'learner_credit' }, + ); + expect(listSubsidyAccessPoliciesSpy).not.toHaveBeenCalled(); + + expect(logError).toHaveBeenCalledWith(mockListSubsidiesError); + } + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + budgets: [], + canManageLearnerCredit: false, + }, + }), + ); + }); + + it.each('should set `canManageLearnerCredit` to false if no budgets are found', async () => { + fetchEnterpriseOffersSpy.mockResolvedValue({ + data: { + results: [], + }, + }); + getSubsidyByCustomerUUIDSpy.mockResolvedValue({ + data: { + results: [], + }, + }); + listSubsidyAccessPoliciesSpy.mockResolvedValue({ + data: { + results: [], + }, + }); + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen: true, + enterpriseId: TEST_ENTERPRISE_UUID, + }), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + budgets: [], + canManageLearnerCredit: false, + }, + }), + ); + }); + + it('should return the active enterprise offer or subsidy when multiple available', async () => { + const mockSubsidyServiceResponse = [ + { + uuid: 'offer-1', + title: 'offer-name', + active_datetime: '2005-05-15T19:56:09Z', + expiration_datetime: '2006-05-15T19:56:09Z', + is_active: false, + }, + { + uuid: 'offer-2', + title: 'offer-name-2', + active_datetime: '2006-05-15T19:56:09Z', + expiration_datetime: '2099-05-15T19:56:09Z', + is_active: true, + }, + ]; + const mockOfferData = [ + { + id: 'offer-1', + name: 'offer-name', + start: '2005-05-15T19:56:09Z', + end: '2006-05-15T19:56:09Z', + isCurrent: false, + source: BUDGET_TYPES.subsidy, + }, + { + id: 'offer-2', + name: 'offer-name-2', + start: '2006-05-15T19:56:09Z', + end: '2099-05-15T19:56:09Z', + isCurrent: true, + source: BUDGET_TYPES.subsidy, + }, + ]; + + fetchEnterpriseOffersSpy.mockResolvedValueOnce({ + data: { + results: [], + }, + }); + getSubsidyByCustomerUUIDSpy.mockResolvedValueOnce({ + data: { + results: mockSubsidyServiceResponse, + }, + }); + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen: true, + enterpriseId: TEST_ENTERPRISE_UUID, + }), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(getSubsidyByCustomerUUIDSpy).toHaveBeenCalledTimes(1); + expect(getSubsidyByCustomerUUIDSpy).toHaveBeenCalledWith( + TEST_ENTERPRISE_UUID, + { subsidyType: 'learner_credit' }, + ); + expect(result.current).toEqual( + expect.objectContaining({ + isLoading: false, + data: { + budgets: mockOfferData, + canManageLearnerCredit: true, + }, + }), + ); + }); +}); + +describe('useCustomerAgreement', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch customer agreement for the enterprise', async () => { + const mockCustomerAgreement = { + subscriptions: [], + }; + LicenseManagerApiService.fetchCustomerAgreementData.mockResolvedValueOnce({ + data: { + results: [mockCustomerAgreement], + }, + }); + const { result, waitForNextUpdate } = renderHook(() => useCustomerAgreement({ + enterpriseId: TEST_ENTERPRISE_UUID, + })); + + await waitForNextUpdate(); + + expect(LicenseManagerApiService.fetchCustomerAgreementData).toHaveBeenCalledWith({ + enterprise_customer_uuid: TEST_ENTERPRISE_UUID, + }); + expect(result.current).toEqual({ + customerAgreement: mockCustomerAgreement, + isLoading: false, + }); + }); + + it('should should not set customer agreement if results are empty', async () => { + LicenseManagerApiService.fetchCustomerAgreementData.mockResolvedValueOnce({ + data: { + results: [], + }, + }); + const { result, waitForNextUpdate } = renderHook(() => useCustomerAgreement({ + enterpriseId: TEST_ENTERPRISE_UUID, + })); + + await waitForNextUpdate(); + + expect(LicenseManagerApiService.fetchCustomerAgreementData).toHaveBeenCalled(); + expect(result.current).toEqual({ + customerAgreement: undefined, + isLoading: false, + }); + }); +}); + +describe('useCoupons', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch coupons for the enterprise', async () => { + const mockCoupons = [{ uuid: 'test-coupon' }]; + EcommerceApiService.fetchCouponOrders.mockResolvedValueOnce({ + data: { + results: mockCoupons, + }, + }); + const { result, waitForNextUpdate } = renderHook(() => useCoupons()); + + await waitForNextUpdate(); + + expect(EcommerceApiService.fetchCouponOrders).toHaveBeenCalled(); + expect(result.current).toEqual({ + coupons: mockCoupons, + isLoading: false, + }); + }); +}); diff --git a/src/components/EnterpriseSubsidiesContext/data/tests/index.test.js b/src/components/EnterpriseSubsidiesContext/data/tests/index.test.js index 2157455f44..2915b49296 100644 --- a/src/components/EnterpriseSubsidiesContext/data/tests/index.test.js +++ b/src/components/EnterpriseSubsidiesContext/data/tests/index.test.js @@ -9,37 +9,58 @@ jest.mock('../hooks'); const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; describe('useEnterpriseSubsidiesContext', () => { - const basicProps = { enablePortalLearnerCreditManagementScreen: true, enterpriseId: TEST_ENTERPRISE_UUID }; + const basicProps = { + enablePortalLearnerCreditManagementScreen: true, + enterpriseId: TEST_ENTERPRISE_UUID, + }; it.each([ { - offers: [{ uuid: 'offer-id' }], + isLoadingBudgets: false, + budgets: [{ uuid: 'offer-id' }], customerAgreement: { subscriptions: [{ uuid: 'subscription-id' }] }, coupons: [{ uuid: 'coupon-id' }], expectedEnterpriseSubsidyTypes: [ - SUBSIDY_TYPES.offer, + SUBSIDY_TYPES.budget, SUBSIDY_TYPES.coupon, - SUBSIDY_TYPES.license], + SUBSIDY_TYPES.license, + ], }, { - offers: [], + isLoadingBudgets: true, + budgets: undefined, customerAgreement: { subscriptions: [{ uuid: 'subscription-id' }] }, coupons: [{ uuid: 'coupon-id' }], expectedEnterpriseSubsidyTypes: [ SUBSIDY_TYPES.coupon, - SUBSIDY_TYPES.license], + SUBSIDY_TYPES.license, + ], }, { - offers: [], + isLoadingBudgets: false, + budgets: [], + customerAgreement: { subscriptions: [{ uuid: 'subscription-id' }] }, + coupons: [{ uuid: 'coupon-id' }], + expectedEnterpriseSubsidyTypes: [ + SUBSIDY_TYPES.coupon, + SUBSIDY_TYPES.license, + ], + }, + { + isLoadingBudgets: false, + budgets: [], customerAgreement: { subscriptions: [{ uuid: 'subscription-id' }] }, coupons: [], expectedEnterpriseSubsidyTypes: [SUBSIDY_TYPES.license], }, - ])('returns the correct enterpriseSubsidyTypes', ({ - offers, customerAgreement, coupons, expectedEnterpriseSubsidyTypes, + ])('returns the correct enterpriseSubsidyTypes (%s)', ({ + isLoadingBudgets, budgets, customerAgreement, coupons, expectedEnterpriseSubsidyTypes, }) => { - hooks.useEnterpriseOffers.mockReturnValue({ - offers, + hooks.useEnterpriseBudgets.mockReturnValue({ + data: isLoadingBudgets ? undefined : { + budgets, + canManageLearnerCredit: !!budgets.length, + }, }); hooks.useCustomerAgreement.mockReturnValue({ customerAgreement, diff --git a/src/components/EnterpriseSubsidiesContext/index.jsx b/src/components/EnterpriseSubsidiesContext/index.jsx index 59d4a540bd..234e1fad14 100644 --- a/src/components/EnterpriseSubsidiesContext/index.jsx +++ b/src/components/EnterpriseSubsidiesContext/index.jsx @@ -1,15 +1,26 @@ import { createContext, useMemo } from 'react'; import { SUBSIDY_TYPES } from '../../data/constants/subsidyTypes'; -import { useCoupons, useCustomerAgreement, useEnterpriseOffers } from './data/hooks'; +import { useCoupons, useCustomerAgreement, useEnterpriseBudgets } from './data/hooks'; export const EnterpriseSubsidiesContext = createContext(); -export const useEnterpriseSubsidiesContext = ({ enablePortalLearnerCreditManagementScreen, enterpriseId }) => { +export const useEnterpriseSubsidiesContext = ({ + enablePortalLearnerCreditManagementScreen, + enterpriseId, + isTopDownAssignmentEnabled, +}) => { const { - offers, - canManageLearnerCredit, - isLoading: isLoadingOffers, - } = useEnterpriseOffers({ enablePortalLearnerCreditManagementScreen, enterpriseId }); + isLoading: isLoadingBudgets, + data: budgetsOverview, + } = useEnterpriseBudgets({ + enablePortalLearnerCreditManagementScreen, + enterpriseId, + isTopDownAssignmentEnabled, + }); + const { + budgets = [], + canManageLearnerCredit = false, + } = budgetsOverview || {}; const { customerAgreement, @@ -24,8 +35,8 @@ export const useEnterpriseSubsidiesContext = ({ enablePortalLearnerCreditManagem const enterpriseSubsidyTypes = useMemo(() => { const subsidyTypes = []; - if (offers.length > 0) { - subsidyTypes.push(SUBSIDY_TYPES.offer); + if (budgets.length > 0) { + subsidyTypes.push(SUBSIDY_TYPES.budget); } if (coupons.length > 0) { @@ -36,18 +47,18 @@ export const useEnterpriseSubsidiesContext = ({ enablePortalLearnerCreditManagem subsidyTypes.push(SUBSIDY_TYPES.license); } return subsidyTypes; - }, [offers.length, coupons.length, customerAgreement]); + }, [budgets.length, coupons.length, customerAgreement]); - const isLoading = isLoadingOffers || isLoadingCustomerAgreement || isLoadingCoupons; + const isLoading = isLoadingBudgets || isLoadingCustomerAgreement || isLoadingCoupons; const context = useMemo(() => ({ - offers, + budgets, customerAgreement, coupons, canManageLearnerCredit, enterpriseSubsidyTypes, isLoading, - }), [offers, customerAgreement, coupons, canManageLearnerCredit, enterpriseSubsidyTypes, isLoading]); + }), [budgets, customerAgreement, coupons, canManageLearnerCredit, enterpriseSubsidyTypes, isLoading]); return context; }; diff --git a/src/components/learner-credit-management/BudgetCard.jsx b/src/components/learner-credit-management/BudgetCard.jsx index 41ae121150..db5478548c 100644 --- a/src/components/learner-credit-management/BudgetCard.jsx +++ b/src/components/learner-credit-management/BudgetCard.jsx @@ -1,42 +1,60 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useOfferSummary } from './data'; +import { useSubsidySummaryAnalyticsApi } from './data'; import SubBudgetCard from './SubBudgetCard'; import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; /** - * Renders one or more budget cards for the given offer (enterprise or Subsidy from enterprise-subsidy). If the offer is - * an enterprise offer, it will render a single card. If the offer is a Subsidy, it will render one card for - * each associated budget. + * Renders one or more budget cards for the given budget. If the budget is + * an enterprise offer, it will render a single card. If the budget is a Subsidy, + * it will render one card for each associated budget. If the budget is a Policy, + * it will also render a single card. + * + * @param {Object} budget Represents either: + * - Enterprise Offer (ecommerce) + * - Subsidy (enterprise-subsidy) + * - Policy (enterprise-access) * - * @param {*} offer Represents either an enterprise offer or a Subsidy (enterprise-subsidy). * @returns Budget card component(s). */ const BudgetCard = ({ - offer, + budget, enterpriseUUID, enterpriseSlug, - offerType, - displayName, }) => { - const { start, end } = offer; - const { - isLoading: isLoadingOfferSummary, - offerSummary, - } = useOfferSummary(enterpriseUUID, offer); + isLoading: isLoadingSubsidySummaryAnalyticsApi, + subsidySummary: subsidySummaryAnalyticsApi, + } = useSubsidySummaryAnalyticsApi(enterpriseUUID, budget); + + // Subsidy Access Policies will always have a single budget, so we can render a single card + // without relying on `useSubsidySummaryAnalyticsApi`. + if (budget.source === BUDGET_TYPES.policy) { + return ( + + ); + } - // Enterprise Offers will always have a single budget, so we can render a single card. - if (offerType === BUDGET_TYPES.ecommerce) { + // Enterprise Offers (ecommerce) will always have a single budget, so we can render a single card. + if (budget.source === BUDGET_TYPES.ecommerce) { return ( ); @@ -44,37 +62,41 @@ const BudgetCard = ({ // We're now dealing with a Subsidy (enterprise-subsidy), but the analytics API isn't aware of any // associated budgets; nothing should display. - if (!offerSummary?.budgetsSummary) { + if (!subsidySummaryAnalyticsApi?.budgetsSummary) { return null; } - // Render a card for each associated budget with the Subsidy (enterprise-subsidy) - return offerSummary.budgetsSummary.map((budget) => ( + // Render a card for each associated Budget (Policy, enterprise-access) with the Subsidy (enterprise-subsidy) + return subsidySummaryAnalyticsApi.budgetsSummary.map((subBudget) => ( )); }; BudgetCard.propTypes = { - offer: PropTypes.shape({ - id: PropTypes.string.isRequired, + budget: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, name: PropTypes.string.isRequired, start: PropTypes.string.isRequired, end: PropTypes.string.isRequired, + source: PropTypes.oneOf(Object.values(BUDGET_TYPES)).isRequired, + aggregates: PropTypes.shape({ + available: PropTypes.number.isRequired, + spent: PropTypes.number.isRequired, + pending: PropTypes.number, + }), }).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, - offerType: PropTypes.oneOf(Object.values(BUDGET_TYPES)).isRequired, - displayName: PropTypes.string, }; export default BudgetCard; diff --git a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx index a9e114744f..6cfa8d0d1b 100644 --- a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx +++ b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx @@ -3,15 +3,15 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; -import { useBudgetId, useOfferRedemptions } from './data'; +import { useBudgetId, useBudgetRedemptions } from './data'; const BudgetDetailRedemptions = ({ enterpriseFeatures, enterpriseUUID }) => { const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId(); const { isLoading, - offerRedemptions, - fetchOfferRedemptions, - } = useOfferRedemptions( + budgetRedemptions, + fetchBudgetRedemptions, + } = useBudgetRedemptions( enterpriseUUID, enterpriseOfferId, subsidyAccessPolicyId, @@ -31,8 +31,8 @@ const BudgetDetailRedemptions = ({ enterpriseFeatures, enterpriseUUID }) => {

    ); diff --git a/src/components/learner-credit-management/MultipleBudgetsPage.jsx b/src/components/learner-credit-management/MultipleBudgetsPage.jsx index fe12ab2719..b8bb186def 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPage.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPage.jsx @@ -7,12 +7,12 @@ import { Card, Hyperlink, Container, + Skeleton, } from '@edx/paragon'; import { connect } from 'react-redux'; import { Helmet } from 'react-helmet'; -import Hero from '../Hero'; -import LoadingMessage from '../LoadingMessage'; +import Hero from '../Hero'; import MultipleBudgetsPicker from './MultipleBudgetsPicker'; import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; @@ -25,13 +25,19 @@ const MultipleBudgetsPage = ({ enterpriseSlug, enableLearnerPortal, }) => { - const { offers, isLoading } = useContext(EnterpriseSubsidiesContext); + const { budgets, isLoading } = useContext(EnterpriseSubsidiesContext); if (isLoading) { - return ; + return ( + <> +

    + + Loading budgets... + + ); } - if (offers.length === 0) { + if (budgets.length === 0) { return ( @@ -66,7 +72,7 @@ const MultipleBudgetsPage = ({ { - const orderedOffers = orderOffers(offers); - + const orderedBudgets = orderBudgets(budgets); return ( @@ -25,15 +24,13 @@ const MultipleBudgetsPicker = ({ - {orderedOffers?.map(offer => ( + {orderedBudgets.map(budget => ( ))} @@ -44,7 +41,7 @@ const MultipleBudgetsPicker = ({ }; MultipleBudgetsPicker.propTypes = { - offers: PropTypes.arrayOf(PropTypes.shape()).isRequired, + budgets: PropTypes.arrayOf(PropTypes.shape()).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, enableLearnerPortal: PropTypes.bool.isRequired, diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index 3841aebea3..943090426a 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -18,6 +18,7 @@ const SubBudgetCard = ({ start, end, available, + pending, spent, displayName, enterpriseSlug, @@ -40,7 +41,7 @@ const SubBudgetCard = ({ const subtitle = ( {budgetLabel.status} - + {budgetLabel.term} {formattedDate} @@ -50,7 +51,6 @@ const SubBudgetCard = ({ ( + const renderCardSection = () => ( Balance} muted > - - Available - {formatPrice(availableBalance)} + +
    Available
    + {formatPrice(available)} - - Spent - {formatPrice(spentBalance)} + {pending > 0 && ( + +
    Pending
    + {formatPrice(pending)} + + )} + +
    Spent
    + {formatPrice(spent)}
    @@ -84,8 +90,10 @@ const SubBudgetCard = ({ isLoading={isLoading} > - {renderCardHeader(displayName || 'Overview', id)} - {budgetLabel.status !== BUDGET_STATUSES.scheduled && renderCardSection(available, spent)} + + {renderCardHeader(displayName || 'Overview', id)} + {budgetLabel.status !== BUDGET_STATUSES.scheduled && renderCardSection()} + ); @@ -93,12 +101,13 @@ const SubBudgetCard = ({ SubBudgetCard.propTypes = { enterpriseSlug: PropTypes.string.isRequired, - id: PropTypes.string, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), start: PropTypes.string, end: PropTypes.string, spent: PropTypes.number, isLoading: PropTypes.bool, available: PropTypes.number, + pending: PropTypes.number, displayName: PropTypes.string, }; diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 7f20e7e8aa..149a7418f0 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -120,14 +120,21 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { mutate(mutationArgs, { onSuccess: ({ created, noChange, updated }) => { setAssignButtonState('complete'); + // Ensure the budget and budgets queries are invalidated so that the relevant + // queries become stale and refetches new updated data from the API. queryClient.invalidateQueries({ queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), }); + queryClient.invalidateQueries({ + queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseId), + }); handleCloseAssignmentModal(); onSuccessEnterpriseTrackEvents({ created, noChange, updated, }); displayToastForAssignmentAllocation({ totalLearnersAssigned: learnerEmails.length }); + + // Navigate to the activity tab history.push(pathToActivityTab); }, onError: (err) => { diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 42697fc761..2e90beae99 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -487,10 +487,13 @@ describe('Course card works as expected', () => { } } else { // Verify success state - expect(mockInvalidateQueries).toHaveBeenCalledTimes(1); + expect(mockInvalidateQueries).toHaveBeenCalledTimes(2); expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: learnerCreditManagementQueryKeys.budget(mockSubsidyAccessPolicy.uuid), }); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseUUID), + }); expect(getButtonElement('Assigned', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'true'); await waitFor(() => { // Verify all modals close (error modal + assignment modal) diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 11dd744498..004b905118 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -68,7 +68,7 @@ export const ASSIGNMENT_STATUS_MODAL_MAX_WIDTH = 480; // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const learnerCreditManagementQueryKeys = { all: ['learner-credit-management'], - budgets: () => [...learnerCreditManagementQueryKeys.all, 'budgets'], + budgets: (enterpriseId) => [...learnerCreditManagementQueryKeys.all, 'budgets', enterpriseId], budget: (budgetId) => [...learnerCreditManagementQueryKeys.all, 'budget', budgetId], budgetActivity: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'activity'], budgetActivityOverview: (budgetId) => [...learnerCreditManagementQueryKeys.budgetActivity(budgetId), 'overview'], diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index 6da548a12b..61731bd38e 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -1,6 +1,6 @@ export { default as useBudgetDetailTabs } from './useBudgetDetailTabs'; -export { default as useOfferSummary } from './useOfferSummary'; -export { default as useOfferRedemptions } from './useOfferRedemptions'; +export { default as useSubsidySummaryAnalyticsApi } from './useSubsidySummaryAnalyticsApi'; +export { default as useBudgetRedemptions } from './useBudgetRedemptions'; export { default as useBudgetContentAssignments } from './useBudgetContentAssignments'; export { default as useBudgetId } from './useBudgetId'; export { default as useSubsidyAccessPolicy } from './useSubsidyAccessPolicy'; diff --git a/src/components/learner-credit-management/data/hooks/tests/useOfferRedemptions.test.jsx b/src/components/learner-credit-management/data/hooks/tests/useBudgetRedemptions.test.jsx similarity index 91% rename from src/components/learner-credit-management/data/hooks/tests/useOfferRedemptions.test.jsx rename to src/components/learner-credit-management/data/hooks/tests/useBudgetRedemptions.test.jsx index 5e8d29a495..dbf59cc0d9 100644 --- a/src/components/learner-credit-management/data/hooks/tests/useOfferRedemptions.test.jsx +++ b/src/components/learner-credit-management/data/hooks/tests/useBudgetRedemptions.test.jsx @@ -2,7 +2,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook } from '@testing-library/react-hooks/dom'; import { camelCaseObject } from '@edx/frontend-platform/utils'; -import useOfferRedemptions from '../useOfferRedemptions'; +import useBudgetRedemptions from '../useBudgetRedemptions'; import useSubsidyAccessPolicy from '../useSubsidyAccessPolicy'; import EnterpriseDataApiService from '../../../../../data/services/EnterpriseDataApiService'; import SubsidyApiService from '../../../../../data/services/EnterpriseSubsidyApiService'; @@ -57,7 +57,7 @@ const wrapper = ({ children }) => ( {children} ); -describe('useOfferRedemptions', () => { +describe('useBudgetRedemptions', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -88,21 +88,21 @@ describe('useOfferRedemptions', () => { useSubsidyAccessPolicy.mockReturnValue({ data: { subsidyUuid } }); const { result, waitForNextUpdate } = renderHook( - () => useOfferRedemptions(TEST_ENTERPRISE_UUID, offerId, budgetId, isTopDownAssignmentEnabled), + () => useBudgetRedemptions(TEST_ENTERPRISE_UUID, offerId, budgetId, isTopDownAssignmentEnabled), { wrapper }, ); expect(result.current).toMatchObject({ - offerRedemptions: { + budgetRedemptions: { itemCount: 0, pageCount: 0, results: [], }, isLoading: true, - fetchOfferRedemptions: expect.any(Function), + fetchBudgetRedemptions: expect.any(Function), }); act(() => { - result.current.fetchOfferRedemptions({ + result.current.fetchBudgetRedemptions({ pageIndex: 0, // `DataTable` uses zero-based indexing pageSize: 20, sortBy: [ @@ -151,13 +151,13 @@ describe('useOfferRedemptions', () => { }] : camelCaseObject(mockOfferEnrollments); expect(result.current).toMatchObject({ - offerRedemptions: { + budgetRedemptions: { itemCount: 100, pageCount: 5, results: mockExpectedResultsObj, }, isLoading: false, - fetchOfferRedemptions: expect.any(Function), + fetchBudgetRedemptions: expect.any(Function), }); }); }); diff --git a/src/components/learner-credit-management/data/hooks/tests/useOfferSummary.test.js b/src/components/learner-credit-management/data/hooks/tests/useOfferSummary.test.js deleted file mode 100644 index 40ccc6df01..0000000000 --- a/src/components/learner-credit-management/data/hooks/tests/useOfferSummary.test.js +++ /dev/null @@ -1,67 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks/dom'; - -import useOfferSummary from '../useOfferSummary'; -import EnterpriseDataApiService from '../../../../../data/services/EnterpriseDataApiService'; - -jest.mock('@edx/frontend-platform/config', () => ({ - getConfig: jest.fn(() => ({ - FEATURE_LEARNER_CREDIT_MANAGEMENT: true, - })), -})); -jest.mock('../../../../../data/services/EnterpriseDataApiService'); - -const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; -const TEST_ENTERPRISE_OFFER_ID = 1; - -const mockOfferSummary = { - offer_id: TEST_ENTERPRISE_OFFER_ID, - status: 'Open', - enterprise_customer_uuid: TEST_ENTERPRISE_UUID, - amount_of_offer_spent: 200.00, - max_discount: 5000.00, - percent_of_offer_spent: 0.04, - remaining_balance: 4800.00, -}; -const mockEnterpriseOffer = { - id: TEST_ENTERPRISE_OFFER_ID, -}; - -describe('useOfferSummary', () => { - it('should handle null enterprise offer', async () => { - const { result } = renderHook(() => useOfferSummary(TEST_ENTERPRISE_UUID)); - - expect(result.current).toEqual({ - offerSummary: undefined, - isLoading: false, - }); - }); - - it('should fetch summary data for enterprise offer', async () => { - EnterpriseDataApiService.fetchEnterpriseOfferSummary.mockResolvedValueOnce({ data: mockOfferSummary }); - const { result, waitForNextUpdate } = renderHook(() => useOfferSummary(TEST_ENTERPRISE_UUID, mockEnterpriseOffer)); - - expect(result.current).toEqual({ - offerSummary: undefined, - isLoading: true, - }); - - await waitForNextUpdate(); - - expect(EnterpriseDataApiService.fetchEnterpriseOfferSummary).toHaveBeenCalled(); - const expectedResult = { - totalFunds: 5000, - redeemedFunds: 200, - redeemedFundsExecEd: NaN, - redeemedFundsOcm: NaN, - remainingFunds: 4800, - percentUtilized: 0.04, - offerId: 1, - budgetsSummary: [], - offerType: undefined, - }; - expect(result.current).toEqual({ - offerSummary: expectedResult, - isLoading: false, - }); - }); -}); diff --git a/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js b/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js new file mode 100644 index 0000000000..320f51d685 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js @@ -0,0 +1,93 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; +import { logError } from '@edx/frontend-platform/logging'; + +import useSubsidySummaryAnalyticsApi from '../useSubsidySummaryAnalyticsApi'; +import EnterpriseDataApiService from '../../../../../data/services/EnterpriseDataApiService'; + +jest.mock('@edx/frontend-platform/config', () => ({ + ...jest.requireActual('@edx/frontend-platform/config'), + getConfig: jest.fn(() => ({ + FEATURE_LEARNER_CREDIT_MANAGEMENT: true, + })), +})); +jest.mock('@edx/frontend-platform/logging', () => ({ + ...jest.requireActual('@edx/frontend-platform/logging'), + logError: jest.fn(), +})); +jest.mock('../../../../../data/services/EnterpriseDataApiService'); + +const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; +const TEST_ENTERPRISE_OFFER_ID = 1; + +const mockOfferSummary = { + offer_id: TEST_ENTERPRISE_OFFER_ID, + status: 'Open', + enterprise_customer_uuid: TEST_ENTERPRISE_UUID, + amount_of_offer_spent: 200.00, + max_discount: 5000.00, + percent_of_offer_spent: 0.04, + remaining_balance: 4800.00, +}; +const mockEnterpriseOffer = { + id: TEST_ENTERPRISE_OFFER_ID, +}; + +describe('useSubsidySummaryAnalyticsApi', () => { + it('should handle null enterprise offer', async () => { + const { result } = renderHook(() => useSubsidySummaryAnalyticsApi(TEST_ENTERPRISE_UUID)); + + expect(result.current).toEqual({ + offerSummary: undefined, + isLoading: false, + }); + }); + + it.each([ + { shouldThrowApiException: false }, + { shouldThrowApiException: true }, + ])('should fetch summary data for enterprise offer (%s)', async ({ shouldThrowApiException }) => { + const mockFetchError = 'mock fetch error'; + if (shouldThrowApiException) { + EnterpriseDataApiService.fetchEnterpriseOfferSummary.mockRejectedValueOnce(mockFetchError); + } else { + EnterpriseDataApiService.fetchEnterpriseOfferSummary.mockResolvedValueOnce({ data: mockOfferSummary }); + } + const { + result, + waitForNextUpdate, + } = renderHook(() => useSubsidySummaryAnalyticsApi(TEST_ENTERPRISE_UUID, mockEnterpriseOffer)); + + expect(result.current).toEqual({ + subsidySummary: undefined, + isLoading: true, + }); + + await waitForNextUpdate(); + + expect(EnterpriseDataApiService.fetchEnterpriseOfferSummary).toHaveBeenCalled(); + + if (shouldThrowApiException) { + expect(logError).toHaveBeenCalledWith(mockFetchError); + expect(result.current).toEqual({ + subsidySummary: undefined, + isLoading: false, + }); + } else { + const expectedResult = { + totalFunds: 5000, + redeemedFunds: 200, + redeemedFundsExecEd: NaN, + redeemedFundsOcm: NaN, + remainingFunds: 4800, + percentUtilized: 0.04, + offerId: 1, + budgetsSummary: [], + offerType: undefined, + }; + expect(result.current).toEqual({ + subsidySummary: expectedResult, + isLoading: false, + }); + } + }); +}); diff --git a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js b/src/components/learner-credit-management/data/hooks/useBudgetRedemptions.js similarity index 91% rename from src/components/learner-credit-management/data/hooks/useOfferRedemptions.js rename to src/components/learner-credit-management/data/hooks/useBudgetRedemptions.js index 64618cae8b..bb3ed2fbf5 100644 --- a/src/components/learner-credit-management/data/hooks/useOfferRedemptions.js +++ b/src/components/learner-credit-management/data/hooks/useBudgetRedemptions.js @@ -46,7 +46,7 @@ const applyFiltersToOptions = (filters, options, shouldFetchSubsidyTransactions } }; -const useOfferRedemptions = ( +const useBudgetRedemptions = ( enterpriseUUID, offerId = null, budgetId = null, @@ -54,14 +54,14 @@ const useOfferRedemptions = ( ) => { const shouldTrackFetchEvents = useRef(false); const [isLoading, setIsLoading] = useState(true); - const [offerRedemptions, setOfferRedemptions] = useState({ + const [budgetRedemptions, setBudgetRedemptions] = useState({ itemCount: 0, pageCount: 0, results: [], }); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(budgetId); - const fetchOfferRedemptions = useCallback((args) => { + const fetchBudgetRedemptions = useCallback((args) => { const fetch = async () => { try { const shouldFetchSubsidyTransactions = budgetId && isTopDownAssignmentEnabled; @@ -103,7 +103,7 @@ const useOfferRedemptions = ( transformedTableResults = transformUtilizationTableResults(data.results); } - setOfferRedemptions({ + setBudgetRedemptions({ itemCount: data.count, pageCount: data.numPages, results: transformedTableResults, @@ -139,13 +139,16 @@ const useOfferRedemptions = ( subsidyAccessPolicy?.subsidyUuid, ]); - const debouncedFetchOfferRedemptions = useMemo(() => debounce(fetchOfferRedemptions, 300), [fetchOfferRedemptions]); + const debouncedFetchBudgetRedemptions = useMemo( + () => debounce(fetchBudgetRedemptions, 300), + [fetchBudgetRedemptions], + ); return { isLoading, - offerRedemptions, - fetchOfferRedemptions: debouncedFetchOfferRedemptions, + budgetRedemptions, + fetchBudgetRedemptions: debouncedFetchBudgetRedemptions, }; }; -export default useOfferRedemptions; +export default useBudgetRedemptions; diff --git a/src/components/learner-credit-management/data/hooks/useOfferSummary.js b/src/components/learner-credit-management/data/hooks/useOfferSummary.js deleted file mode 100644 index 95a90b348b..0000000000 --- a/src/components/learner-credit-management/data/hooks/useOfferSummary.js +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useState } from 'react'; -import { camelCaseObject } from '@edx/frontend-platform/utils'; -import { logError } from '@edx/frontend-platform/logging'; - -import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; -import { transformOfferSummary } from '../utils'; - -const useOfferSummary = (enterpriseUUID, enterpriseOffer) => { - const [isLoading, setIsLoading] = useState(true); - const [offerSummary, setOfferSummary] = useState(); - - useEffect(() => { - if (!enterpriseOffer) { - setIsLoading(false); - return; - } - - const fetchData = async () => { - try { - setIsLoading(true); - const response = await EnterpriseDataApiService.fetchEnterpriseOfferSummary(enterpriseUUID, enterpriseOffer.id); - const data = camelCaseObject(response.data); - const transformedOfferSummary = transformOfferSummary(data); - setOfferSummary(transformedOfferSummary); - } catch (error) { - logError(error); - } finally { - setIsLoading(false); - } - }; - - fetchData(); - }, [enterpriseUUID, enterpriseOffer]); - - return { - isLoading, - offerSummary, - }; -}; - -export default useOfferSummary; diff --git a/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js b/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js new file mode 100644 index 0000000000..1dde1b995c --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; +import { logError } from '@edx/frontend-platform/logging'; + +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; +import { transformSubsidySummary } from '../utils'; +import { BUDGET_TYPES } from '../../../EnterpriseApp/data/constants'; + +const useSubsidySummaryAnalyticsApi = (enterpriseUUID, budget) => { + const [isLoading, setIsLoading] = useState(true); + const [subsidySummary, setSubsidySummary] = useState(); + + useEffect(() => { + // If there is no budget, or the budget is an ecommerce offer or subsidy, fetch the + // subsidy summary data from the analytics API. + if (!budget || [BUDGET_TYPES.ecommerce, BUDGET_TYPES.subsidy].includes(budget.source)) { + setIsLoading(false); + return; + } + + const fetchData = async () => { + try { + setIsLoading(true); + const response = await EnterpriseDataApiService.fetchEnterpriseOfferSummary( + enterpriseUUID, + budget.id, + ); + const data = camelCaseObject(response.data); + const transformedSubsidySummary = transformSubsidySummary(data); + setSubsidySummary(transformedSubsidySummary); + } catch (error) { + logError(error); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [enterpriseUUID, budget]); + + return { + isLoading, + subsidySummary, + }; +}; + +export default useSubsidySummaryAnalyticsApi; diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js index fa4ec2bb23..060f26a979 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -1,13 +1,13 @@ -import { transformOfferSummary, getBudgetStatus, orderOffers } from '../utils'; +import { transformSubsidySummary, getBudgetStatus, orderBudgets } from '../utils'; import { EXEC_ED_OFFER_TYPE } from '../constants'; -describe('transformOfferSummary', () => { - it('should return null if there is no offerSummary', () => { - expect(transformOfferSummary()).toBeNull(); +describe('transformSubsidySummary', () => { + it('should return null if there is no budgetSummary', () => { + expect(transformSubsidySummary()).toBeNull(); }); it('should safeguard against bad data', () => { - const offerSummary = { + const budgetSummary = { maxDiscount: 1, amountOfOfferSpent: 1.34, remainingBalance: -0.34, @@ -15,7 +15,7 @@ describe('transformOfferSummary', () => { offerType: EXEC_ED_OFFER_TYPE, }; - expect(transformOfferSummary(offerSummary)).toEqual({ + expect(transformSubsidySummary(budgetSummary)).toEqual({ totalFunds: 1, redeemedFunds: 1, redeemedFundsExecEd: NaN, @@ -29,7 +29,7 @@ describe('transformOfferSummary', () => { }); it('should handle when no maxDiscount is not set', () => { - const offerSummary = { + const budgetSummary = { maxDiscount: null, amountOfOfferSpent: 100, remainingBalance: null, @@ -39,7 +39,7 @@ describe('transformOfferSummary', () => { budgetsSummary: [], }; - expect(transformOfferSummary(offerSummary)).toEqual({ + expect(transformSubsidySummary(budgetSummary)).toEqual({ totalFunds: null, redeemedFunds: 100, remainingFunds: null, @@ -53,7 +53,7 @@ describe('transformOfferSummary', () => { }); it('should handle when budgetsSummary is provided', () => { - const offerSummary = { + const budgetSummary = { maxDiscount: 1000, amountOfOfferSpent: 500, remainingBalance: 500, @@ -71,7 +71,7 @@ describe('transformOfferSummary', () => { }], }; - expect(transformOfferSummary(offerSummary)).toEqual({ + expect(transformSubsidySummary(budgetSummary)).toEqual({ totalFunds: 1000, redeemedFunds: 500, remainingFunds: 500, @@ -118,52 +118,52 @@ describe('getBudgetStatus', () => { }); }); -// Example offer objects for testing -const offers = [ +// Example Budget objects for testing +const budgets = [ { - name: 'Offer 1', + name: 'Budget 1', start: '2023-01-01T00:00:00Z', end: '2023-01-10T00:00:00Z', }, { - name: 'Offer 2', + name: 'Budget 2', start: '2022-12-01T00:00:00Z', end: '2022-12-20T00:00:00Z', }, { - name: 'Offer 3', + name: 'Budget 3', start: '2023-02-01T00:00:00Z', end: '2023-02-15T00:00:00Z', }, { - name: 'Offer 4', + name: 'Budget 4', start: '2023-01-15T00:00:00Z', end: '2023-01-25T00:00:00Z', }, ]; -describe('orderOffers', () => { +describe('orderBudgets', () => { it('should sort offers correctly', () => { - const sortedOffers = orderOffers(offers); + const sortedBudgets = orderBudgets(budgets); - // Expected order: Active offers (Offer 2), Upcoming offers (Offer 1, Offer 4), Expired offers (Offer 3) - expect(sortedOffers.map((offer) => offer.name)).toEqual(['Offer 2', 'Offer 1', 'Offer 4', 'Offer 3']); + // Expected order: Active budgets (Budget 2), Upcoming budgets (Budget 1, Budget 4), Expired budgets (Budget 3) + expect(sortedBudgets.map((budget) => budget.name)).toEqual(['Budget 2', 'Budget 1', 'Budget 4', 'Budget 3']); }); it('should handle empty input', () => { - const sortedOffers = orderOffers([]); - expect(sortedOffers).toEqual([]); + const sortedBudgets = orderBudgets([]); + expect(sortedBudgets).toEqual([]); }); it('should handle offers with the same status and end date', () => { - const duplicateOffers = [ - { name: 'Offer A', start: '2023-01-01T00:00:00Z', end: '2023-01-15T00:00:00Z' }, - { name: 'Offer B', start: '2023-01-01T00:00:00Z', end: '2023-01-15T00:00:00Z' }, + const duplicateBudgets = [ + { name: 'Budget A', start: '2023-01-01T00:00:00Z', end: '2023-01-15T00:00:00Z' }, + { name: 'Budget B', start: '2023-01-01T00:00:00Z', end: '2023-01-15T00:00:00Z' }, ]; - const sortedOffers = orderOffers(duplicateOffers); + const sortedBudgets = orderBudgets(duplicateBudgets); // Since both offers have the same status ("active") and end date, they should be sorted alphabetically by name. - expect(sortedOffers.map((offer) => offer.name)).toEqual(['Offer A', 'Offer B']); + expect(sortedBudgets.map((budget) => budget.name)).toEqual(['Budget A', 'Budget B']); }); }); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index a055fd567c..51746f3eca 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -14,17 +14,20 @@ import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiSe import SubsidyApiService from '../../../data/services/EnterpriseSubsidyApiService'; /** - * Transforms offer summary from API for display in the UI, guarding + * Transforms subsidy (offer or Subsidy) summary from API for display in the UI, guarding * against bad data (e.g., accounting for refunds). * - * @param {object} offerSummary Object containing summary about an offer. - * @returns Object containing transformed summary about an enterprise offer. + * @param {object} subsidySummary Object containing summary about a budget. + * @returns Object containing transformed summary about a budget. */ -export const transformOfferSummary = (offerSummary) => { - if (!offerSummary) { return null; } +export const transformSubsidySummary = (subsidySummary) => { + if (!subsidySummary) { + return null; + } + const budgetsSummary = []; - if (offerSummary?.budgets) { - const budgets = offerSummary?.budgets; + if (subsidySummary?.budgets) { + const budgets = subsidySummary?.budgets; for (let i = 0; i < budgets.length; i++) { const redeemedFunds = budgets[i].amountOfPolicySpent && parseFloat(budgets[i].amountOfPolicySpent); const remainingFunds = budgets[i].remainingBalance && parseFloat(budgets[i].remainingBalance); @@ -37,10 +40,10 @@ export const transformOfferSummary = (offerSummary) => { } } - const totalFunds = offerSummary.maxDiscount && parseFloat(offerSummary.maxDiscount); - let redeemedFunds = offerSummary.amountOfOfferSpent && parseFloat(offerSummary.amountOfOfferSpent); - let redeemedFundsOcm = offerSummary.amountOfferSpentOcm && parseFloat(offerSummary.amountOfferSpentOcm); - let redeemedFundsExecEd = offerSummary.amountOfferSpentExecEd && parseFloat(offerSummary.amountOfferSpentExecEd); + const totalFunds = subsidySummary.maxDiscount && parseFloat(subsidySummary.maxDiscount); + let redeemedFunds = subsidySummary.amountOfOfferSpent && parseFloat(subsidySummary.amountOfOfferSpent); + let redeemedFundsOcm = subsidySummary.amountOfferSpentOcm && parseFloat(subsidySummary.amountOfferSpentOcm); + let redeemedFundsExecEd = subsidySummary.amountOfferSpentExecEd && parseFloat(subsidySummary.amountOfferSpentExecEd); // cap redeemed funds at the maximum funds available (`maxDiscount`), if applicable, so we // don't display redeemed funds > funds available. @@ -50,19 +53,20 @@ export const transformOfferSummary = (offerSummary) => { redeemedFundsExecEd = Math.min(redeemedFundsExecEd, totalFunds); } - let remainingFunds = offerSummary.remainingBalance && parseFloat(offerSummary.remainingBalance); + let remainingFunds = subsidySummary.remainingBalance && parseFloat(subsidySummary.remainingBalance); // prevent remaining funds from going below $0, if applicable. if (remainingFunds) { remainingFunds = Math.max(remainingFunds, 0.0); } - let percentUtilized = offerSummary.percentOfOfferSpent && parseFloat(offerSummary.percentOfOfferSpent); + let percentUtilized = subsidySummary.percentOfOfferSpent && parseFloat(subsidySummary.percentOfOfferSpent); // prevent percent utilized from going over 1.0, if applicable. if (percentUtilized) { percentUtilized = Math.min(percentUtilized, 1.0); } - const { offerType } = offerSummary; - const { offerId } = offerSummary; + const { offerType } = subsidySummary; + const { offerId } = subsidySummary; + return { totalFunds, redeemedFunds, @@ -172,36 +176,36 @@ export const formatPrice = (price, options = {}) => { }; /** - * Orders a list of offers based on their status, end date, and name. - * Active offers come first, followed by scheduled offers, and then expired offers. - * Within each status, offers are sorted by their end date and name. + * Orders a list of budgets based on their status, end date, and name. + * Active budgets come first, followed by scheduled budgets, and then expired budgets. + * Within each status, budgets are sorted by their end date and name. * - * @param {Array} offers - An array of offer objects. - * @returns {Array} - The sorted array of offer objects. + * @param {Array} budgets - An array of budget objects. + * @returns {Array} - The sorted array of budget objects. */ -export const orderOffers = (offers) => { +export const orderBudgets = (budgets) => { const statusOrder = { Active: 0, Scheduled: 1, Expired: 2, }; - offers?.sort((offerA, offerB) => { - const statusA = getBudgetStatus(offerA.start, offerA.end).status; - const statusB = getBudgetStatus(offerB.start, offerB.end).status; + budgets?.sort((budgetA, budgetB) => { + const statusA = getBudgetStatus(budgetA.start, budgetA.end).status; + const statusB = getBudgetStatus(budgetB.start, budgetB.end).status; if (statusOrder[statusA] !== statusOrder[statusB]) { return statusOrder[statusA] - statusOrder[statusB]; } - if (offerA.end !== offerB.end) { - return offerA.end.localeCompare(offerB.end); + if (budgetA.end !== budgetB.end) { + return budgetA.end.localeCompare(budgetB.end); } - return offerA.name.localeCompare(offerB.name); + return budgetA.name.localeCompare(budgetB.name); }); - return offers; + return budgets; }; /** diff --git a/src/components/learner-credit-management/index.jsx b/src/components/learner-credit-management/index.jsx index 0eaef07494..14f99a54f8 100644 --- a/src/components/learner-credit-management/index.jsx +++ b/src/components/learner-credit-management/index.jsx @@ -5,7 +5,7 @@ import MultipleBudgetsPage from './MultipleBudgetsPage'; import BudgetDetailPage from './BudgetDetailPage'; const LearnerCreditManagementRoutes = ({ match }) => ( -
    +
    ( path={`${match.path}/:budgetId/:activeTabKey?`} component={BudgetDetailPage} /> -
    + ); LearnerCreditManagementRoutes.propTypes = { diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 7ddc100c8e..739e8e8996 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -13,43 +13,44 @@ import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import BudgetCard from '../BudgetCard'; -import { useOfferSummary, useOfferRedemptions } from '../data'; +import { formatPrice, useSubsidySummaryAnalyticsApi, useBudgetRedemptions } from '../data'; import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; jest.mock('../data', () => ({ ...jest.requireActual('../data'), - useOfferSummary: jest.fn(), - useOfferRedemptions: jest.fn(), + useSubsidySummaryAnalyticsApi: jest.fn(), + useBudgetRedemptions: jest.fn(), })); -useOfferSummary.mockReturnValue({ +useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, offerSummary: null, }); -useOfferRedemptions.mockReturnValue({ +useBudgetRedemptions.mockReturnValue({ isLoading: false, offerRedemptions: { itemCount: 0, pageCount: 0, results: [], }, - fetchOfferRedemptions: jest.fn(), + fetchBudgetRedemptions: jest.fn(), }); const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); -const enterpriseId = 'test-enterprise'; +const enterpriseSlug = 'test-enterprise'; const enterpriseUUID = '1234'; const initialStore = { portalConfiguration: { - enterpriseId, + enterpriseId: enterpriseUUID, + enterpriseSlug, }, }; const store = getMockStore({ ...initialStore }); -const mockEnterpriseOfferId = '123'; -const mockEnterpriseOfferEnrollmentId = 456; +const mockEnterpriseOfferId = 123; +const mockBudgetUuid = 'test-budget-uuid'; -const mockOfferDisplayName = 'Test Enterprise Offer'; +const mockBudgetDisplayName = 'Test Enterprise Budget Display Name'; const BudgetCardWrapper = ({ ...rest }) => ( @@ -62,164 +63,181 @@ const BudgetCardWrapper = ({ ...rest }) => ( ); describe('', () => { - describe('with enterprise offer', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - it('displays correctly for Offers', () => { - const mockOffer = { - id: mockEnterpriseOfferId, - name: mockOfferDisplayName, - start: '2022-01-01', - end: '2023-01-01', - }; - const mockOfferRedemption = { - created: '2022-02-01', - enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, - }; - useOfferSummary.mockReturnValue({ - isLoading: false, - offerSummary: { - totalFunds: 5000, - redeemedFunds: 200, - remainingFunds: 4800, - percentUtilized: 0.04, - offerType: 'Site', - budgetsSummary: [ - { - id: 123, - start: '2022-01-01', - end: '2022-01-01', - available: 200, - spent: 100, - enterpriseSlug: enterpriseId, - }, - ], - }, - }); - useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: { - results: [mockOfferRedemption], - itemCount: 1, - pageCount: 1, - }, - fetchOfferRedemptions: jest.fn(), - }); - render(); - expect(screen.getByText('Overview')); - expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); - const formattedString = `Expired ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; - const elementsWithTestId = screen.getAllByTestId('offer-date'); - const firstElementWithTestId = elementsWithTestId[0]; - expect(firstElementWithTestId).toHaveTextContent(formattedString); + it('displays correctly for Enterprise Offers (ecommerce)', () => { + const mockBudget = { + id: mockEnterpriseOfferId, + name: mockBudgetDisplayName, + start: '2022-01-01', + end: '2023-01-01', + source: BUDGET_TYPES.ecommerce, + }; + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: { + totalFunds: mockBudgetAggregates.total, + redeemedFunds: mockBudgetAggregates.spent, + remainingFunds: mockBudgetAggregates.available, + percentUtilized: mockBudgetAggregates.spent / mockBudgetAggregates.total, + offerType: 'Site', + offerId: mockEnterpriseOfferId, + budgetsSummary: [], + }, }); - it('renders SubBudgetCard when offerType is ecommerce', () => { - const mockOffer = { - id: mockEnterpriseOfferId, - name: mockOfferDisplayName, - start: '2022-01-01', - end: '2023-01-01', - offerType: BUDGET_TYPES.ecommerce, - }; - const mockOfferRedemption = { - created: '2022-02-01', - enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, - }; - useOfferSummary.mockReturnValue({ - isLoading: false, - offerSummary: { - totalFunds: 5000, - redeemedFunds: 200, - remainingFunds: 4800, - percentUtilized: 0.04, - offerType: 'learner_credit', - budgetsSummary: [ - { - id: 123, - start: '2022-01-01', - end: '2022-01-01', - available: 200, - spent: 100, - enterpriseSlug: enterpriseId, - }, - ], - }, - }); - useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: { - results: [mockOfferRedemption], - itemCount: 1, - pageCount: 1, - }, - fetchOfferRedemptions: jest.fn(), - }); - - render(); - - expect(screen.getByTestId('view-budget')).toBeInTheDocument(); + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Expired ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + + // View budget CTA + const viewBudgetCTA = screen.getByText('View budget', { selector: 'a' }); + expect(viewBudgetCTA).toBeInTheDocument(); + expect(viewBudgetCTA).toHaveAttribute('href', `/${enterpriseSlug}/admin/learner-credit/${mockEnterpriseOfferId}`); + + // Aggregates + expect(screen.getByText('Balance')).toBeInTheDocument(); + expect(screen.getByText('Available')).toBeInTheDocument(); + expect(screen.getByText(formatPrice(mockBudgetAggregates.available))).toBeInTheDocument(); + expect(screen.getByText('Spent')).toBeInTheDocument(); + expect(screen.getByText(formatPrice(mockBudgetAggregates.spent))).toBeInTheDocument(); + }); + + it('displays correctly for Subsidy (enterprise-subsidy)', () => { + const mockBudget = { + id: mockEnterpriseOfferId, + name: mockBudgetDisplayName, + start: '2022-01-01', + end: '2023-01-01', + source: BUDGET_TYPES.subsidy, + }; + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: { + totalFunds: mockBudgetAggregates.total, + redeemedFunds: mockBudgetAggregates.spent, + remainingFunds: mockBudgetAggregates.available, + percentUtilized: mockBudgetAggregates.spent / mockBudgetAggregates.total, + offerType: 'Site', + offerId: mockEnterpriseOfferId, + budgetsSummary: [ + { + id: 'test-subsidy-uuid', + start: '2022-01-01', + end: '2022-01-01', + remainingFunds: mockBudgetAggregates.available, + redeemedFunds: mockBudgetAggregates.spent, + enterpriseSlug, + subsidyAccessPolicyDisplayName: mockBudgetDisplayName, + subsidyAccessPolicyUuid: mockBudgetUuid, + }, + ], + }, }); - it('renders SubBudgetCard when offerType is not ecommerce', () => { - const mockOffer = { - id: mockEnterpriseOfferId, - name: mockOfferDisplayName, - start: '2022-01-01', - end: '2023-01-01', - offerType: 'otherOfferType', - }; - const mockOfferRedemption = { - created: '2022-02-01', - enterpriseEnrollmentId: mockEnterpriseOfferEnrollmentId, - }; - useOfferSummary.mockReturnValue({ - isLoading: false, - offerSummary: { - totalFunds: 5000, - redeemedFunds: 200, - remainingFunds: 4800, - percentUtilized: 0.04, - offerType: 'learner_credit', - budgetsSummary: [ - { - id: 123, - start: '2022-01-01', - end: '2022-01-01', - available: 200, - spent: 100, - enterpriseSlug: enterpriseId, - }, - ], - }, - }); - useOfferRedemptions.mockReturnValue({ - isLoading: false, - offerRedemptions: { - results: [mockOfferRedemption], - itemCount: 1, - pageCount: 1, - }, - fetchOfferRedemptions: jest.fn(), - }); - - render(); - - expect(screen.getByTestId('view-budget')).toBeInTheDocument(); + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Expired ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + + // View budget CTA + const viewBudgetCTA = screen.getByText('View budget', { selector: 'a' }); + expect(viewBudgetCTA).toBeInTheDocument(); + expect(viewBudgetCTA).toHaveAttribute('href', `/${enterpriseSlug}/admin/learner-credit/${mockBudgetUuid}`); + + // Aggregates + expect(screen.getByText('Balance')).toBeInTheDocument(); + expect(screen.getByText('Available')).toBeInTheDocument(); + expect(screen.getByText(formatPrice(mockBudgetAggregates.available))).toBeInTheDocument(); + expect(screen.getByText('Spent')).toBeInTheDocument(); + expect(screen.getByText(formatPrice(mockBudgetAggregates.spent))).toBeInTheDocument(); + }); + + it.each([ + { isAssignableBudget: false }, + { isAssignableBudget: true }, + ])('displays correctly for Policy (enterprise-access) (%s)', ({ isAssignableBudget }) => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + pending: 100, + available: isAssignableBudget ? 4700 : 4800, + }; + const mockBudget = { + id: mockBudgetUuid, + name: mockBudgetDisplayName, + start: '2022-01-01', + end: '2023-01-01', + source: BUDGET_TYPES.policy, + aggregates: { + available: mockBudgetAggregates.available, + pending: isAssignableBudget ? mockBudgetAggregates.pending : undefined, + spent: mockBudgetAggregates.spent, + }, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: undefined, }); + + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Expired ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + + // View budget CTA + const viewBudgetCTA = screen.getByText('View budget', { selector: 'a' }); + expect(viewBudgetCTA).toBeInTheDocument(); + expect(viewBudgetCTA).toHaveAttribute('href', `/${enterpriseSlug}/admin/learner-credit/${mockBudgetUuid}`); + + // Aggregates + expect(screen.getByText('Balance')).toBeInTheDocument(); + expect(screen.getByText('Available')).toBeInTheDocument(); + expect(screen.getByText(formatPrice(mockBudgetAggregates.available))).toBeInTheDocument(); + if (isAssignableBudget) { + expect(screen.getByText('Pending')).toBeInTheDocument(); + expect(screen.getByText(formatPrice(mockBudgetAggregates.pending))).toBeInTheDocument(); + } else { + expect(screen.queryByText('Pending')).not.toBeInTheDocument(); + } + expect(screen.getByText('Spent')).toBeInTheDocument(); + expect(screen.getByText(formatPrice(mockBudgetAggregates.spent))).toBeInTheDocument(); }); }); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 170b33b0ad..5138d10080 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -16,7 +16,7 @@ import { faker } from '@faker-js/faker'; import BudgetDetailPage from '../BudgetDetailPage'; import { useSubsidyAccessPolicy, - useOfferRedemptions, + useBudgetRedemptions, useBudgetContentAssignments, useBudgetDetailActivityOverview, useIsLargeOrGreater, @@ -46,7 +46,7 @@ jest.mock('react-router-dom', () => ({ jest.mock('../data', () => ({ ...jest.requireActual('../data'), - useOfferRedemptions: jest.fn(), + useBudgetRedemptions: jest.fn(), useBudgetContentAssignments: jest.fn(), useSubsidyAccessPolicy: jest.fn(), useBudgetDetailActivityOverview: jest.fn(), @@ -77,7 +77,7 @@ const mockEmptyStateBudgetDetailActivityOverview = { contentAssignments: { count: 0 }, spentTransactions: { count: 0 }, }; -const mockEmptyOfferRedemptions = { +const mockEmptyBudgetRedemptions = { itemCount: 0, pageCount: 0, results: [], @@ -292,10 +292,10 @@ describe('', () => { }, fetchContentAssignments: jest.fn(), }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); const storeState = { ...initialStoreState, @@ -309,8 +309,8 @@ describe('', () => { }; renderWithRouter(); - expect(useOfferRedemptions).toHaveBeenCalledTimes(1); - expect(useOfferRedemptions).toHaveBeenCalledWith(...expectedUseOfferRedemptionsArgs); + expect(useBudgetRedemptions).toHaveBeenCalledTimes(1); + expect(useBudgetRedemptions).toHaveBeenCalledWith(...expectedUseOfferRedemptionsArgs); // Activity tab exists and is active expect(screen.getByText('Activity').getAttribute('aria-selected')).toBe('true'); @@ -357,14 +357,14 @@ describe('', () => { }, fetchContentAssignments: jest.fn(), }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: { + budgetRedemptions: { itemCount: 2, pageCount: 1, results: [mockEnrollmentTransaction, mockEnrollmentTransactionWithReversal], }, - fetchOfferRedemptions: jest.fn(), + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -427,10 +427,10 @@ describe('', () => { }, fetchContentAssignments: mockFetchContentAssignments, }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -507,10 +507,10 @@ describe('', () => { }, fetchContentAssignments: mockFetchContentAssignments, }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -588,10 +588,10 @@ describe('', () => { }, fetchContentAssignments: mockFetchContentAssignments, }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -676,10 +676,10 @@ describe('', () => { }, fetchContentAssignments: mockFetchContentAssignments, }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -741,10 +741,10 @@ describe('', () => { }, fetchContentAssignments: jest.fn(), }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -864,10 +864,10 @@ describe('', () => { }, fetchContentAssignments: jest.fn(), }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -927,10 +927,10 @@ describe('', () => { spentTransactions: { count: 0 }, }, }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -965,10 +965,10 @@ describe('', () => { spentTransactions: { count: 0 }, }, }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); renderWithRouter(); @@ -1106,10 +1106,10 @@ describe('', () => { budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', }); - useOfferRedemptions.mockReturnValue({ + useBudgetRedemptions.mockReturnValue({ isLoading: false, - offerRedemptions: mockEmptyOfferRedemptions, - fetchOfferRedemptions: jest.fn(), + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), }); useSubsidyAccessPolicy.mockReturnValue({ isInitialLoading: false, diff --git a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx index 701e12dbb3..7b922bba84 100644 --- a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx +++ b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx @@ -27,11 +27,11 @@ const store = getMockStore({ ...initialStore }); const enterpriseUUID = '1234'; const emptyOffersContextValue = { - offers: [], // Empty offers array + budgets: [], // Empty offers array }; const defaultEnterpriseSubsidiesContextValue = { - offers: [{ + budgets: [{ source: 'subsidy', id: '392f1fe1-ee91-4f44-b174-13ecf59866eb', name: 'Subsidy 2 for Executive Education (2U) Integration QA', @@ -76,6 +76,6 @@ describe('', () => { enterpriseSlug={enterpriseSlug} enterpriseSubsidiesContextValue={enterpriseSubsidiesContextValue} />); - expect(screen.getByText('Loading...')).toBeInTheDocument(); + expect(screen.getByText('Loading budgets...')).toBeInTheDocument(); }); }); diff --git a/src/components/subsidy-requests/tests/SubsidyRequestsContext.test.jsx b/src/components/subsidy-requests/tests/SubsidyRequestsContext.test.jsx index d9b66ca7f3..2d831ff7e5 100644 --- a/src/components/subsidy-requests/tests/SubsidyRequestsContext.test.jsx +++ b/src/components/subsidy-requests/tests/SubsidyRequestsContext.test.jsx @@ -71,7 +71,7 @@ describe('useSubsidyRequestsContext', () => { () => useSubsidyRequestsContext({ enterpriseId: TEST_ENTERPRISE_UUID, enterpriseSubsidyTypes: [ - SUBSIDY_TYPES.offer, SUBSIDY_TYPES.license, + SUBSIDY_TYPES.budget, SUBSIDY_TYPES.license, ], }), ); diff --git a/src/containers/EnterpriseApp/index.jsx b/src/containers/EnterpriseApp/index.jsx index 787876e5d3..b090f6e975 100644 --- a/src/containers/EnterpriseApp/index.jsx +++ b/src/containers/EnterpriseApp/index.jsx @@ -22,6 +22,7 @@ const mapStateToProps = (state) => { enablePortalLearnerCreditManagementScreen: state.portalConfiguration.enablePortalLearnerCreditManagementScreen, enterpriseId: state.portalConfiguration.enterpriseId, enterpriseName: state.portalConfiguration.enterpriseName, + enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, enterpriseBranding: state.portalConfiguration.enterpriseBranding, loading: state.portalConfiguration.loading, }; diff --git a/src/data/constants/subsidyTypes.js b/src/data/constants/subsidyTypes.js index e8d60b8d78..36267a913d 100644 --- a/src/data/constants/subsidyTypes.js +++ b/src/data/constants/subsidyTypes.js @@ -2,5 +2,5 @@ export const SUBSIDY_TYPES = { coupon: 'coupon', license: 'license', - offer: 'offer', + budget: 'budget', }; diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index d0035b3e63..94e4fcae95 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -164,14 +164,30 @@ class EnterpriseAccessApiService { return EnterpriseAccessApiService.apiClient().get(url); } + static listSubsidyAccessPolicies(enterpriseCustomerId) { + const queryParams = new URLSearchParams({ + enterprise_customer_uuid: enterpriseCustomerId, + }); + const url = `${EnterpriseAccessApiService.baseUrl}/subsidy-access-policies/?${queryParams.toString()}`; + return EnterpriseAccessApiService.apiClient().get(url); + } + /** * Retrieve a specific subsidy access policy. + * @param {string} subsidyAccessPolicyUUID The UUID of the subsidy access policy to retrieve. + * @returns {Promise} - A promise that resolves to the response from the API. */ static retrieveSubsidyAccessPolicy(subsidyAccessPolicyUUID) { const url = `${EnterpriseAccessApiService.baseUrl}/subsidy-access-policies/${subsidyAccessPolicyUUID}/`; return EnterpriseAccessApiService.apiClient().get(url); } + /** + * ALlocates assignments for a specific subsidy access policy. + * @param {String} subsidyAccessPolicyUUID The UUID of the subsidy access policy to allocate content assignments for. + * @param {Object} payload The metadata to send to the API, including learner emails and the content key. + * @returns {Promise} - A promise that resolves to the response from the API. + */ static allocateContentAssignments(subsidyAccessPolicyUUID, payload) { const url = `${EnterpriseAccessApiService.baseUrl}/policy-allocation/${subsidyAccessPolicyUUID}/allocate/`; return EnterpriseAccessApiService.apiClient().post(url, payload); diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index a6a5aeec7f..7317e14639 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -162,6 +162,13 @@ describe('EnterpriseAccessApiService', () => { ); }); + test('listSubsidyAccessPolicies calls enterprise-access to fetch subsidy access policies', () => { + EnterpriseAccessApiService.listSubsidyAccessPolicies(mockEnterpriseUUID); + expect(axios.get).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/subsidy-access-policies/?enterprise_customer_uuid=${mockEnterpriseUUID}`, + ); + }); + test('retrieveSubsidyAccessPolicy calls enterprise-access to fetch subsidy access policy', () => { EnterpriseAccessApiService.retrieveSubsidyAccessPolicy(mockSubsidyAccessPolicyUUID); expect(axios.get).toBeCalledWith( From 0be2be7772c307a85203b218c8d367f170e54ca6 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 1 Dec 2023 13:35:21 -0500 Subject: [PATCH 088/124] fix: resolve bug with calling analytics api for budget cards (#1111) --- .../useSubsidySummaryAnalyticsApi.test.js | 108 ++++++++++++------ .../hooks/useSubsidySummaryAnalyticsApi.js | 4 +- 2 files changed, 74 insertions(+), 38 deletions(-) diff --git a/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js b/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js index 320f51d685..3fa26919c9 100644 --- a/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js +++ b/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js @@ -3,6 +3,7 @@ import { logError } from '@edx/frontend-platform/logging'; import useSubsidySummaryAnalyticsApi from '../useSubsidySummaryAnalyticsApi'; import EnterpriseDataApiService from '../../../../../data/services/EnterpriseDataApiService'; +import { BUDGET_TYPES } from '../../../../EnterpriseApp/data/constants'; jest.mock('@edx/frontend-platform/config', () => ({ ...jest.requireActual('@edx/frontend-platform/config'), @@ -18,6 +19,7 @@ jest.mock('../../../../../data/services/EnterpriseDataApiService'); const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid'; const TEST_ENTERPRISE_OFFER_ID = 1; +const TEST_ENTERPRISE_BUDGET_UUID = 'test-enterprise-budget-uuid'; const mockOfferSummary = { offer_id: TEST_ENTERPRISE_OFFER_ID, @@ -28,11 +30,14 @@ const mockOfferSummary = { percent_of_offer_spent: 0.04, remaining_balance: 4800.00, }; -const mockEnterpriseOffer = { - id: TEST_ENTERPRISE_OFFER_ID, -}; describe('useSubsidySummaryAnalyticsApi', () => { + const mockFetchEnterpriseOfferSummarySpy = jest.spyOn(EnterpriseDataApiService, 'fetchEnterpriseOfferSummary'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should handle null enterprise offer', async () => { const { result } = renderHook(() => useSubsidySummaryAnalyticsApi(TEST_ENTERPRISE_UUID)); @@ -43,51 +48,82 @@ describe('useSubsidySummaryAnalyticsApi', () => { }); it.each([ - { shouldThrowApiException: false }, - { shouldThrowApiException: true }, - ])('should fetch summary data for enterprise offer (%s)', async ({ shouldThrowApiException }) => { + { + budgetId: TEST_ENTERPRISE_OFFER_ID, + budgetType: BUDGET_TYPES.ecommerce, + shouldCallApi: true, + shouldThrowApiException: false, + }, + { + budgetId: TEST_ENTERPRISE_BUDGET_UUID, + budgetType: BUDGET_TYPES.subsidy, + shouldCallApi: true, + shouldThrowApiException: true, + }, + { + budgetId: TEST_ENTERPRISE_BUDGET_UUID, + budgetType: BUDGET_TYPES.policy, + shouldCallApi: false, + shouldThrowApiException: false, + }, + ])('should fetch summary data for enterprise offer (%s)', async ({ + budgetId, + budgetType, + shouldThrowApiException, + shouldCallApi, + }) => { const mockFetchError = 'mock fetch error'; if (shouldThrowApiException) { - EnterpriseDataApiService.fetchEnterpriseOfferSummary.mockRejectedValueOnce(mockFetchError); + mockFetchEnterpriseOfferSummarySpy.mockRejectedValue(mockFetchError); } else { - EnterpriseDataApiService.fetchEnterpriseOfferSummary.mockResolvedValueOnce({ data: mockOfferSummary }); + mockFetchEnterpriseOfferSummarySpy.mockResolvedValue({ data: mockOfferSummary }); } + const mockBudget = { + id: budgetId, + source: budgetType, + }; + const { result, waitForNextUpdate, - } = renderHook(() => useSubsidySummaryAnalyticsApi(TEST_ENTERPRISE_UUID, mockEnterpriseOffer)); - - expect(result.current).toEqual({ - subsidySummary: undefined, - isLoading: true, - }); - - await waitForNextUpdate(); + } = renderHook(() => useSubsidySummaryAnalyticsApi( + TEST_ENTERPRISE_UUID, + mockBudget, + )); - expect(EnterpriseDataApiService.fetchEnterpriseOfferSummary).toHaveBeenCalled(); - - if (shouldThrowApiException) { - expect(logError).toHaveBeenCalledWith(mockFetchError); + if (shouldCallApi) { expect(result.current).toEqual({ subsidySummary: undefined, - isLoading: false, + isLoading: true, }); + await waitForNextUpdate(); + expect(mockFetchEnterpriseOfferSummarySpy).toHaveBeenCalled(); + + if (shouldThrowApiException) { + expect(logError).toHaveBeenCalledWith(mockFetchError); + expect(result.current).toEqual({ + subsidySummary: undefined, + isLoading: false, + }); + } else { + const expectedResult = { + totalFunds: 5000, + redeemedFunds: 200, + redeemedFundsExecEd: NaN, + redeemedFundsOcm: NaN, + remainingFunds: 4800, + percentUtilized: 0.04, + offerId: 1, + budgetsSummary: [], + offerType: undefined, + }; + expect(result.current).toEqual({ + subsidySummary: expectedResult, + isLoading: false, + }); + } } else { - const expectedResult = { - totalFunds: 5000, - redeemedFunds: 200, - redeemedFundsExecEd: NaN, - redeemedFundsOcm: NaN, - remainingFunds: 4800, - percentUtilized: 0.04, - offerId: 1, - budgetsSummary: [], - offerType: undefined, - }; - expect(result.current).toEqual({ - subsidySummary: expectedResult, - isLoading: false, - }); + expect(EnterpriseDataApiService.fetchEnterpriseOfferSummary).not.toHaveBeenCalled(); } }); }); diff --git a/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js b/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js index 1dde1b995c..f0aaee7826 100644 --- a/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js +++ b/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js @@ -11,9 +11,9 @@ const useSubsidySummaryAnalyticsApi = (enterpriseUUID, budget) => { const [subsidySummary, setSubsidySummary] = useState(); useEffect(() => { - // If there is no budget, or the budget is an ecommerce offer or subsidy, fetch the + // If there is no budget, or the budget is NOT an ecommerce offer or subsidy, fetch the // subsidy summary data from the analytics API. - if (!budget || [BUDGET_TYPES.ecommerce, BUDGET_TYPES.subsidy].includes(budget.source)) { + if (![BUDGET_TYPES.ecommerce, BUDGET_TYPES.subsidy].includes(budget?.source)) { setIsLoading(false); return; } From e0f020bcc623eca59e3ff84e98be96c59ff6e2dc Mon Sep 17 00:00:00 2001 From: edX requirements bot <49161187+edx-requirements-bot@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:39:18 -0500 Subject: [PATCH 089/124] chore: update browserslist DB (#1113) Co-authored-by: abdullahwaheed --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe96608470..eba758d745 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8448,9 +8448,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001564", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", - "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "funding": [ { "type": "opencollective", From 39a006d0209838258c8aea4f3e8411e2582cd94c Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Sat, 2 Dec 2023 07:42:16 +0000 Subject: [PATCH 090/124] feat: adding idp metadata network form error handling --- src/components/forms/FormContext.tsx | 1 + src/components/forms/FormContextWrapper.tsx | 1 + src/components/forms/FormWorkflow.tsx | 10 ++- src/components/forms/data/reducer.test.ts | 4 + src/components/forms/data/reducer.ts | 4 +- .../SettingsLMSTab/LMSFormWorkflowConfig.tsx | 3 + .../SettingsSSOTab/SSOFormWorkflowConfig.tsx | 79 +++++++++++++------ .../steps/NewSSOConfigConfigureStep.tsx | 63 ++++++++++++++- src/components/settings/data/constants.js | 3 + 9 files changed, 142 insertions(+), 26 deletions(-) diff --git a/src/components/forms/FormContext.tsx b/src/components/forms/FormContext.tsx index 9ff41f7cb1..95946a3f00 100644 --- a/src/components/forms/FormContext.tsx +++ b/src/components/forms/FormContext.tsx @@ -22,6 +22,7 @@ export type FormContext = { errorMap?: { [name: string]: string[] }; stateMap?: { [name: string]: any }; currentStep?: FormWorkflowStep; + allSteps?: FormWorkflowStep[]; }; export const FormContextObject: Context = createContext({}); diff --git a/src/components/forms/FormContextWrapper.tsx b/src/components/forms/FormContextWrapper.tsx index fa3c8a2b75..199817ebca 100644 --- a/src/components/forms/FormContextWrapper.tsx +++ b/src/components/forms/FormContextWrapper.tsx @@ -22,6 +22,7 @@ const FormContextWrapper = ({ const initializeAction: InitializeFormArguments = { formFields: formData as FormConfigData, currentStep: formWorkflowConfig.getCurrentStep(), + steps: formWorkflowConfig.steps, }; const [formFieldsState, dispatch] = useReducer< FormReducerType, diff --git a/src/components/forms/FormWorkflow.tsx b/src/components/forms/FormWorkflow.tsx index d17d131548..a60b017e64 100644 --- a/src/components/forms/FormWorkflow.tsx +++ b/src/components/forms/FormWorkflow.tsx @@ -42,6 +42,7 @@ export type FormWorkflowAwaitHandler = { export type FormWorkflowButtonConfig = { buttonText: string; opensNewWindow: boolean; + preventDefaultErrorModal: boolean; onClick?: (args: FormWorkflowHandlerArgs) => Promise | void; awaitSuccess?: FormWorkflowAwaitHandler; }; @@ -136,7 +137,12 @@ const FormWorkflow = ({ setNextInProgress(true); const newFormFields: FormConfigData = await nextButtonConfig.onClick({ formFields, - errHandler: setFormError, + errHandler: (error) => { + setFormError(error); + if (!!error) { + advance = false; + } + }, dispatch, formFieldsChanged: !!isEdited, }); @@ -228,7 +234,7 @@ const FormWorkflow = ({ return ( <> diff --git a/src/components/forms/data/reducer.test.ts b/src/components/forms/data/reducer.test.ts index 988102d634..5bb5f43ee6 100644 --- a/src/components/forms/data/reducer.test.ts +++ b/src/components/forms/data/reducer.test.ts @@ -19,6 +19,7 @@ const dummyButtonConfig: FormWorkflowButtonConfig = { buttonText: 'Unimportant', onClick: ({ formFields }: FormWorkflowHandlerArgs) => Promise.resolve(formFields as DummyFormFields), opensNewWindow: false, + preventDefaultErrorModal: false, }; const createDummyStep = ( @@ -65,6 +66,7 @@ const getTestInitializeFormArguments = () => { formFields: { ...testFormFields }, validations: dummyFormFieldsValidations, currentStep: steps[0], + steps: [...steps], }; return testArgs; }; @@ -77,6 +79,7 @@ describe('Form reducer tests', () => { formFields: { ...formFields }, validations: dummyFormFieldsValidations, currentStep: steps[0], + steps: [...steps], }; expect(initializeForm(initializeFormArguments)).toEqual({ formFields, @@ -84,6 +87,7 @@ describe('Form reducer tests', () => { hasErrors: false, currentStep: steps[0], isEdited: false, + allSteps: [...steps], }); }); diff --git a/src/components/forms/data/reducer.ts b/src/components/forms/data/reducer.ts index afee5e8c8c..b105f53e35 100644 --- a/src/components/forms/data/reducer.ts +++ b/src/components/forms/data/reducer.ts @@ -53,6 +53,7 @@ export type InitializeFormArguments = { formFields: FormFields; validations?: FormFieldValidation[]; currentStep: FormWorkflowStep; + steps: FormWorkflowStep[]; }; export function initializeFormImpl( @@ -78,7 +79,7 @@ export function initializeFormImpl( export function initializeForm(action: InitializeFormArguments) { const initialFormState: Pick< FormContext, - 'isEdited' | 'formFields' | 'currentStep' + 'isEdited' | 'formFields' | 'currentStep' | 'allSteps' > = { isEdited: false }; if (action?.formFields) { initialFormState.formFields = action.formFields; @@ -86,6 +87,7 @@ export function initializeForm(action: InitializeFormArguments {}, + preventDefaultErrorModal: false, }), }, ]; @@ -92,6 +93,7 @@ export const LMSFormWorkflowConfig = ({ nextButtonConfig: () => ({ buttonText: 'Next', opensNewWindow: false, + preventDefaultErrorModal: false, }), }, { @@ -102,6 +104,7 @@ export const LMSFormWorkflowConfig = ({ nextButtonConfig: () => ({ buttonText: 'Next', opensNewWindow: false, + preventDefaultErrorModal: false, }), }, ); diff --git a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx index 2d3cd7c4a1..ca7dd08e07 100644 --- a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx +++ b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx @@ -8,6 +8,8 @@ import SSOConfigConfirmStep from './steps/NewSSOConfigConfirmStep'; import LmsApiService from '../../../data/services/LmsApiService'; import handleErrors from '../utils'; import { snakeCaseDict } from '../../../utils'; +import { AxiosError } from 'axios'; +import { INVALID_IDP_METADATA_ERROR, RECORD_UNDER_CONFIGURATIONS_ERROR } from '../data/constants'; type SSOConfigSnakeCase = { uuid?: string, @@ -25,9 +27,9 @@ type SSOConfigSnakeCase = { email_attribute: string, username_attribute: string, country_attribute: string, - submitted_at: null, - configured_at: null, - validated_at: null, + submitted_at?: null, + configured_at?: null, + validated_at?: null, odata_api_timeout_interval: null, odata_api_root_url: string, odata_company_id: string, @@ -36,10 +38,11 @@ type SSOConfigSnakeCase = { sapsf_private_key: string, odata_client_id: string, oauth_user_id: string, - sp_metadata_url?: string + sp_metadata_url?: string, + record?: object, }; -type SSOConfigCamelCase = { +export type SSOConfigCamelCase = { uuid?: string, enterpriseCustomer: string, isRemoved: boolean, @@ -81,46 +84,72 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { const placeHolderButton = (buttonName?: string) => () => ({ buttonText: buttonName || 'Next', opensNewWindow: false, - onClick: () => {}, + onClick: () => { }, + preventDefaultErrorModal: false, }); + const advanceConnectStep = async ({ + formFields, + errHandler, + }: FormWorkflowHandlerArgs) => { + errHandler?.(''); + return { ...formFields }; + }; + + const sanitizeAndCopyFormFields = (formFields: SSOConfigSnakeCase) => { + const copiedFormFields = { ...formFields }; + return omit(copiedFormFields, ['record', 'sp_metadata_url', 'submitted_at', 'configured_at','validated_at']); + }; + const saveChanges = async ({ formFields, errHandler, + // @ts-ignore:next-line formFieldsChanged is only used in the below TODO formFieldsChanged, - }:FormWorkflowHandlerArgs) => { + }: FormWorkflowHandlerArgs) => { let err = null; - if (!formFieldsChanged) { - // Don't submit if nothing has changed - return formFields; - } + + // TODO : Accurately detect if form fields have changed + // if (!formFieldsChanged && !idpMetadataError) { + // // Don't submit if nothing has changed + // return formFields; + // } let updatedFormFields: SSOConfigCamelCase = omit(formFields, ['idpConnectOption', 'spMetadataUrl', 'isPendingConfiguration']); updatedFormFields.enterpriseCustomer = enterpriseId; const submittedFormFields: SSOConfigSnakeCase = snakeCaseDict(updatedFormFields) as SSOConfigSnakeCase; - if (submittedFormFields?.uuid) { + let copiedFormFields = sanitizeAndCopyFormFields(submittedFormFields); + if (copiedFormFields?.uuid) { try { const updateResponse = await LmsApiService.updateEnterpriseSsoOrchestrationRecord( - submittedFormFields, + copiedFormFields, formFields?.uuid, ); updatedFormFields = updateResponse.data; - } catch (error) { + } catch (error: AxiosError | any) { err = handleErrors(error); - setConfigureError(error); + if (error.message?.includes("Must provide valid IDP metadata url")) { + errHandler?.(INVALID_IDP_METADATA_ERROR); + } else if (error.message?.includes("Record has already been submitted for configuration.")) { + errHandler?.(RECORD_UNDER_CONFIGURATIONS_ERROR); + } else { + setConfigureError(error); + } } } else { try { - const createResponse = await LmsApiService.createEnterpriseSsoOrchestrationRecord(submittedFormFields); + const createResponse = await LmsApiService.createEnterpriseSsoOrchestrationRecord(copiedFormFields); updatedFormFields.uuid = createResponse.data.record; updatedFormFields.spMetadataUrl = createResponse.data.sp_metadata_url; - } catch (error) { + } catch (error: AxiosError | any) { err = handleErrors(error); - setConfigureError(error); + if (error.message?.includes("Must provide valid IDP metadata url")) { + errHandler?.(INVALID_IDP_METADATA_ERROR); + } else { + setConfigureError(error); + } } } - if (err && errHandler) { - errHandler(err); - } + const newFormFields = { ...formFields, ...updatedFormFields } as SSOConfigCamelCase; return newFormFields; }; @@ -131,7 +160,12 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { formComponent: SSOConfigConnectStep, validations: SSOConfigConnectStepValidations, stepName: 'Connect', - nextButtonConfig: placeHolderButton(), + nextButtonConfig: () => ({ + buttonText: 'Next', + opensNewWindow: false, + onClick: advanceConnectStep, + preventDefaultErrorModal: true, + }), }, { index: 1, formComponent: SSOConfigConfigureStep, @@ -141,6 +175,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { buttonText: 'Configure', opensNewWindow: false, onClick: saveChanges, + preventDefaultErrorModal: true, }), showBackButton: true, showCancelButton: false, diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx index 30e347d1ef..ff238dfee2 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { - Form, Container, + Alert, Button, Form, Container, } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; import ValidatedFormControl from '../../../forms/ValidatedFormControl'; import { FormContext, FormFieldValidation, useFormContext } from '../../../forms/FormContext'; import { urlValidation } from '../../../../utils'; +import { FormWorkflowStep } from '../../../forms/FormWorkflow'; +import { FORM_ERROR_MESSAGE, setStepAction } from '../../../forms/data/actions'; +import { INVALID_IDP_METADATA_ERROR, RECORD_UNDER_CONFIGURATIONS_ERROR } from '../../data/constants'; +import { SSOConfigCamelCase } from '../SSOFormWorkflowConfig'; const isSAPConfig = (fields) => fields.identityProvider === 'sap_success_factors'; @@ -35,6 +40,9 @@ export const validations: FormFieldValidation[] = [ const SSOConfigConfigureStep = () => { const { formFields, + dispatch, + allSteps, + stateMap, }: FormContext = useFormContext(); const usingSAP = formFields?.identityProvider === 'sap_success_factors'; @@ -147,12 +155,65 @@ const SSOConfigConfigureStep = () => { ); + const returnToConnectStep = () => { + const connectStep = allSteps?.[0] as FormWorkflowStep; + dispatch?.( + setStepAction({ step: connectStep }) + ); + }; + return (

    Enter integration details

    + {stateMap?.[FORM_ERROR_MESSAGE] === RECORD_UNDER_CONFIGURATIONS_ERROR && ( + + Record under configuration + , + ]} + className="mt-3 mb-3" + dismissible + stacked + icon={Info} + > + Configuration Error +

    + Your record was recently submitted for configuration and must completed before you can resubmit. Please + check back in a few minutes. If the problem persists, contact enterprise customer support. +

    +
    + )} + {stateMap?.[FORM_ERROR_MESSAGE] === INVALID_IDP_METADATA_ERROR && ( + + Return to Connect step + , + ]} + className="mt-3 mb-3" + dismissible + stacked + icon={Info} + > + Metadata Error +

    + Please return to the “Connect” step and verify that your metadata URL or metadata file is correct. After + verifying, please try again. If the problem persists, contact enterprise customer support. +

    +
    + )}

    Set display name

    diff --git a/src/components/settings/data/constants.js b/src/components/settings/data/constants.js index 23915bb12b..8f9747f841 100644 --- a/src/components/settings/data/constants.js +++ b/src/components/settings/data/constants.js @@ -160,3 +160,6 @@ export const PIONEER_THEME = { banner: '#B8EBEF', accent: '#F96E46', }; + +export const INVALID_IDP_METADATA_ERROR = 'Invalid IdP metadata'; +export const RECORD_UNDER_CONFIGURATIONS_ERROR = 'Record under configurations'; From 252f243f15b08c7f2f6698ddf55e2530bee74637 Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Mon, 4 Dec 2023 21:25:34 +0000 Subject: [PATCH 091/124] feat: dynamically using enterprise slug when displaying sso informational alerts --- .../settings/SettingsSSOTab/NewSSOConfigAlerts.jsx | 5 ++++- .../SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx index 26f1e24c05..746c228bbb 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx @@ -18,6 +18,7 @@ const NewSSOConfigAlerts = ({ notConfigured, contactEmail, closeAlerts, + enterpriseSlug, }) => { const dismissSetupCompleteAlert = () => { ssoCookies.set( @@ -63,7 +64,7 @@ const NewSSOConfigAlerts = ({
    1. Copy the URL for your learner Portal dashboard below:

    -   http://courses.edx.org/dashboard?tpa_hint=saml-bestrun-hana
    +   http://courses.edx.org/dashboard?tpa_hint={enterpriseSlug}

    2: Launch a new incognito or private window and paste the copied URL into the URL bar to load your learner Portal dashboard.
    @@ -103,10 +104,12 @@ NewSSOConfigAlerts.propTypes = { notConfigured: PropTypes.arrayOf(PropTypes.shape({})).isRequired, closeAlerts: PropTypes.func.isRequired, contactEmail: PropTypes.string.isRequired, + enterpriseSlug: PropTypes.string.isRequired, }; const mapStateToProps = state => ({ contactEmail: state.portalConfiguration.contactEmail, + enterpriseSlug: state.portalConfiguration.enterpriseSlug, }); export default connect(mapStateToProps)(NewSSOConfigAlerts); diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx index 47c79f90fe..1ca8978463 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx @@ -123,6 +123,12 @@ describe('New SSO Config Alerts Tests', () => { 'You need to test your SSO connection', ), ).toBeInTheDocument(); + expect( + screen.getByText( + 'http://courses.edx.org/dashboard?tpa_hint=sluggy', + { exact: false }, + ), + ).toBeInTheDocument(); expect( screen.queryByText( 'Your SSO integration is live!', From 6c9eb5b95b778be7d3cd6b0e86981d7ed56ba495 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Mon, 4 Dec 2023 17:19:08 -0500 Subject: [PATCH 092/124] feat: background refetching loading state on budgets cards (#1114) --- .../EnterpriseSubsidiesContext/index.jsx | 12 +++- .../SubBudgetCard.jsx | 29 +++++++-- .../cards/NewAssignmentModalButton.jsx | 19 +++--- .../cards/tests/CourseCard.test.jsx | 42 +++++++------ ...seSuccessfulAssignmentToastContextValue.js | 25 ++++++-- .../tests/BudgetCard.test.jsx | 15 ++++- .../tests/BudgetDetailPageWrapper.test.jsx | 60 ++++++++++++++++--- 7 files changed, 155 insertions(+), 47 deletions(-) diff --git a/src/components/EnterpriseSubsidiesContext/index.jsx b/src/components/EnterpriseSubsidiesContext/index.jsx index 234e1fad14..116d494c44 100644 --- a/src/components/EnterpriseSubsidiesContext/index.jsx +++ b/src/components/EnterpriseSubsidiesContext/index.jsx @@ -11,6 +11,7 @@ export const useEnterpriseSubsidiesContext = ({ }) => { const { isLoading: isLoadingBudgets, + isFetching: isFetchingBudgets, data: budgetsOverview, } = useEnterpriseBudgets({ enablePortalLearnerCreditManagementScreen, @@ -58,7 +59,16 @@ export const useEnterpriseSubsidiesContext = ({ canManageLearnerCredit, enterpriseSubsidyTypes, isLoading, - }), [budgets, customerAgreement, coupons, canManageLearnerCredit, enterpriseSubsidyTypes, isLoading]); + isFetchingBudgets, + }), [ + budgets, + customerAgreement, + coupons, + canManageLearnerCredit, + enterpriseSubsidyTypes, + isLoading, + isFetchingBudgets, + ]); return context; }; diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index 943090426a..1a6125bdc4 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -1,3 +1,4 @@ +import { useContext } from 'react'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; @@ -8,10 +9,17 @@ import { Col, Badge, Stack, + Skeleton, } from '@edx/paragon'; import { BUDGET_STATUSES, ROUTE_NAMES } from '../EnterpriseApp/data/constants'; import { formatPrice, getBudgetStatus } from './data/utils'; +import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; + +const BackgroundFetchingWrapper = ({ children }) => { + const { isFetchingBudgets } = useContext(EnterpriseSubsidiesContext); + return {children}; +}; const SubBudgetCard = ({ id, @@ -24,6 +32,7 @@ const SubBudgetCard = ({ enterpriseSlug, isLoading, }) => { + const { isFetchingBudgets } = useContext(EnterpriseSubsidiesContext); const budgetLabel = getBudgetStatus(start, end); const formattedDate = dayjs(budgetLabel?.date).format('MMMM D, YYYY'); @@ -49,8 +58,8 @@ const SubBudgetCard = ({ return ( {budgetType}} + subtitle={{subtitle}} actions={ budgetLabel.status !== BUDGET_STATUSES.scheduled ? renderActions(budgetId) @@ -68,17 +77,23 @@ const SubBudgetCard = ({
    Available
    - {formatPrice(available)} + + {isFetchingBudgets ? : formatPrice(available)} + {pending > 0 && (
    Pending
    - {formatPrice(pending)} + + {isFetchingBudgets ? : formatPrice(pending)} + )}
    Spent
    - {formatPrice(spent)} + + {isFetchingBudgets ? : formatPrice(spent)} +
    @@ -99,6 +114,10 @@ const SubBudgetCard = ({ ); }; +BackgroundFetchingWrapper.propTypes = { + children: PropTypes.node.isRequired, +}; + SubBudgetCard.propTypes = { enterpriseSlug: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 149a7418f0..1c819cffeb 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -89,14 +89,13 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { }, []); const onSuccessEnterpriseTrackEvents = ({ - created, noChange, updated, + totalLearnersAllocated, + totalLearnersAlreadyAllocated, }) => { const trackEventMetadata = { ...sharedEnterpriseTrackEventMetadata, - totalAllocatedLearners: learnerEmails.length, - created: created.length, - noChange: noChange.length, - updated: updated.length, + totalLearnersAllocated, + totalLearnersAlreadyAllocated, }; sendEnterpriseTrackEvent( enterpriseId, @@ -129,10 +128,16 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseId), }); handleCloseAssignmentModal(); + const totalLearnersAllocated = created.length + updated.length; + const totalLearnersAlreadyAllocated = noChange.length; onSuccessEnterpriseTrackEvents({ - created, noChange, updated, + totalLearnersAllocated, + totalLearnersAlreadyAllocated, + }); + displayToastForAssignmentAllocation({ + totalLearnersAllocated, + totalLearnersAlreadyAllocated, }); - displayToastForAssignmentAllocation({ totalLearnersAssigned: learnerEmails.length }); // Navigate to the activity tab history.push(pathToActivityTab); diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 2e90beae99..73f6d19784 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -103,7 +103,7 @@ const mockSubsidyAccessPolicy = { spendAvailableUsd: 50000, }, }; -const mockLearnerEmails = ['hello@example.com', 'world@example.com']; +const mockLearnerEmails = ['hello@example.com', 'world@example.com', 'dinesh@example.com']; const mockDisplaySuccessfulAssignmentToast = jest.fn(); const defaultBudgetDetailPageContextValue = { @@ -315,6 +315,22 @@ describe('Course card works as expected', () => { allocationExceptionReason, shouldRetryAllocationAfterException, }) => { + const mockUpdatedLearnerAssignments = [mockLearnerEmails[0]]; + const mockNoChangeLearnerAssignments = [mockLearnerEmails[1]]; + const mockCreatedLearnerAssignments = mockLearnerEmails.slice(2).map(learnerEmail => ({ + uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f', + assignment_configuration: 'fd456a98-653b-41e9-94d1-94d7b136832a', + learner_email: learnerEmail, + lms_user_id: 0, + content_key: 'string', + content_title: 'string', + content_quantity: 0, + state: 'allocated', + transaction_uuid: '3a6bcbed-b7dc-4791-84fe-b20f12be4001', + last_notification_at: '2019-08-24T14:15:22Z', + actions: [], + })); + if (hasAllocationException) { // mock Axios error mockAllocateContentAssignments.mockRejectedValue({ @@ -326,21 +342,9 @@ describe('Course card works as expected', () => { } else { mockAllocateContentAssignments.mockResolvedValue({ data: { - updated: [], - created: mockLearnerEmails.map(learnerEmail => ({ - uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f', - assignment_configuration: 'fd456a98-653b-41e9-94d1-94d7b136832a', - learner_email: learnerEmail, - lms_user_id: 0, - content_key: 'string', - content_title: 'string', - content_quantity: 0, - state: 'allocated', - transaction_uuid: '3a6bcbed-b7dc-4791-84fe-b20f12be4001', - last_notification_at: '2019-08-24T14:15:22Z', - actions: [], - })), - no_change: [], + updated: mockUpdatedLearnerAssignments, + created: mockCreatedLearnerAssignments, + no_change: mockNoChangeLearnerAssignments, }, }); } @@ -408,6 +412,7 @@ describe('Course card works as expected', () => { const nextSteps = assignmentModal.getByText('Next steps for assigned learners'); userEvent.click(nextSteps); expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(4); + // Verify modal footer expect(assignmentModal.getByText('Help Center: Course Assignments')).toBeInTheDocument(); const cancelAssignmentCTA = getButtonElement('Cancel', { screenOverride: assignmentModal }); @@ -502,7 +507,8 @@ describe('Course card works as expected', () => { // Verify toast notification was displayed expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledTimes(1); expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledWith({ - totalLearnersAssigned: mockLearnerEmails.length, + totalLearnersAllocated: mockCreatedLearnerAssignments.length + mockUpdatedLearnerAssignments.length, + totalLearnersAlreadyAllocated: mockNoChangeLearnerAssignments.length, }); }); } @@ -513,7 +519,7 @@ describe('Course card works as expected', () => { } }); - it.each([ + test.each([ { learnerEmails: ['a@a.com', 'b@bcom', 'c@c.com'], spendAvailableUsd: 1000, diff --git a/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js b/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js index 8d52284152..b86113f604 100644 --- a/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js +++ b/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js @@ -2,30 +2,43 @@ import { useCallback, useMemo, useState } from 'react'; const useSuccessfulAssignmentToastContextValue = () => { const [isToastOpen, setIsToastOpen] = useState(false); - const [learnersAssignedCount, setLearnersAssignedCount] = useState(); + const [learnersAllocatedCount, setLearnersAllocatedCount] = useState(0); + const [learnersAlreadyAllocatedCount, setLearnersAlreadyAllocatedCount] = useState(0); - const handleDisplayToast = useCallback(({ totalLearnersAssigned }) => { + const handleDisplayToast = useCallback(({ totalLearnersAllocated, totalLearnersAlreadyAllocated }) => { + setLearnersAllocatedCount(totalLearnersAllocated); + setLearnersAlreadyAllocatedCount(totalLearnersAlreadyAllocated); setIsToastOpen(true); - setLearnersAssignedCount(totalLearnersAssigned); }, []); const handleCloseToast = useCallback(() => { setIsToastOpen(false); }, []); - const successfulAssignmentAllocationToastMessage = `Course successfully assigned to ${learnersAssignedCount} ${learnersAssignedCount === 1 ? 'learner' : 'learners'}.`; + const pluralizeLearner = (count) => (count === 1 ? 'learner' : 'learners'); + + const toastMessages = []; + if (learnersAllocatedCount > 0) { + toastMessages.push(`Course successfully assigned to ${learnersAllocatedCount} ${pluralizeLearner(learnersAllocatedCount)}.`); + } + if (learnersAlreadyAllocatedCount > 0) { + toastMessages.push(`${learnersAlreadyAllocatedCount} ${pluralizeLearner(learnersAlreadyAllocatedCount)} already had this course assigned.`); + } + const successfulAssignmentAllocationToastMessage = toastMessages.join(' '); const successfulAssignmentToastContextValue = useMemo(() => ({ isSuccessfulAssignmentAllocationToastOpen: isToastOpen, displayToastForAssignmentAllocation: handleDisplayToast, closeToastForAssignmentAllocation: handleCloseToast, - totalLearnersAssigned: learnersAssignedCount, + totalLearnersAllocated: learnersAllocatedCount, + totalLearnersAlreadyAllocated: learnersAlreadyAllocatedCount, successfulAssignmentAllocationToastMessage, }), [ isToastOpen, handleDisplayToast, handleCloseToast, - learnersAssignedCount, + learnersAllocatedCount, + learnersAlreadyAllocatedCount, successfulAssignmentAllocationToastMessage, ]); diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 739e8e8996..50e8972145 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; @@ -15,6 +14,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import BudgetCard from '../BudgetCard'; import { formatPrice, useSubsidySummaryAnalyticsApi, useBudgetRedemptions } from '../data'; import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; +import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; jest.mock('../data', () => ({ ...jest.requireActual('../data'), @@ -52,11 +52,20 @@ const mockBudgetUuid = 'test-budget-uuid'; const mockBudgetDisplayName = 'Test Enterprise Budget Display Name'; -const BudgetCardWrapper = ({ ...rest }) => ( +const defaultEnterpriseSubsidiesContextValue = { + isFetchingBudgets: false, +}; + +const BudgetCardWrapper = ({ + enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue, + ...rest +}) => ( - + + + diff --git a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx index 7207e8f8b4..e219ee25b2 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx @@ -52,11 +52,47 @@ describe('', () => { }); it.each([ - { totalLearnersAssigned: 1, expectedLearnerString: 'learner' }, - { totalLearnersAssigned: 2, expectedLearnerString: 'learners' }, - ])('should render Toast notification for successful assignment allocation (%s)', async ({ - totalLearnersAssigned, - expectedLearnerString, + { + totalLearnersAllocated: 1, + expectedLearnersAllocatedString: 'learner', + totalLearnersAlreadyAllocated: 0, + expectedLearnersAlreadyAllocatedString: undefined, + }, + { + totalLearnersAllocated: 2, + expectedLearnersAllocatedString: 'learners', + totalLearnersAlreadyAllocated: 0, + expectedLearnersAlreadyAllocatedString: undefined, + }, + { + totalLearnersAllocated: 0, + expectedLearnersAllocatedString: undefined, + totalLearnersAlreadyAllocated: 1, + expectedLearnersAlreadyAllocatedString: 'learner', + }, + { + totalLearnersAllocated: 0, + expectedLearnersAllocatedString: undefined, + totalLearnersAlreadyAllocated: 2, + expectedLearnersAlreadyAllocatedString: 'learners', + }, + { + totalLearnersAllocated: 1, + expectedLearnersAllocatedString: 'learner', + totalLearnersAlreadyAllocated: 1, + expectedLearnersAlreadyAllocatedString: 'learner', + }, + { + totalLearnersAllocated: 1, + expectedLearnersAllocatedString: 'learner', + totalLearnersAlreadyAllocated: 1, + expectedLearnersAlreadyAllocatedString: 'learner', + }, + ])('should render Toast notification for successful assignment allocations (%s)', async ({ + totalLearnersAllocated, + expectedLearnersAllocatedString, + totalLearnersAlreadyAllocated, + expectedLearnersAlreadyAllocatedString, }) => { const ToastContextController = () => { const { @@ -65,7 +101,10 @@ describe('', () => { } = useContext(BudgetDetailPageContext); const handleDisplayToast = () => { - displayToastForAssignmentAllocation({ totalLearnersAssigned }); + displayToastForAssignmentAllocation({ + totalLearnersAllocated, + totalLearnersAlreadyAllocated, + }); }; const handleCloseToast = () => { @@ -81,7 +120,14 @@ describe('', () => { }; render(); - const expectedToastMessage = `Course successfully assigned to ${totalLearnersAssigned} ${expectedLearnerString}.`; + const toastMessages = []; + if (totalLearnersAllocated > 0) { + toastMessages.push(`Course successfully assigned to ${totalLearnersAllocated} ${expectedLearnersAllocatedString}.`); + } + if (totalLearnersAlreadyAllocated > 0) { + toastMessages.push(`${totalLearnersAlreadyAllocated} ${expectedLearnersAlreadyAllocatedString} already had this course assigned.`); + } + const expectedToastMessage = toastMessages.join(' '); // Open Toast notification userEvent.click(getButtonElement('Open Toast')); From 2e8f8b888b4ce789cfcfbcdf2c7535a4c0617d2d Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Tue, 5 Dec 2023 11:45:28 -0500 Subject: [PATCH 093/124] feat: persist working with local stage (#1116) --- .env.development-stage | 115 ++++++++++++++++++++++++++++++++++++ package.json | 4 +- webpack.dev-stage.config.js | 26 ++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 .env.development-stage create mode 100644 webpack.dev-stage.config.js diff --git a/.env.development-stage b/.env.development-stage new file mode 100644 index 0000000000..39accee150 --- /dev/null +++ b/.env.development-stage @@ -0,0 +1,115 @@ +# App Specific + +# Note: The Algolia APP_ID and SEARCH_API_KEY are secret and must be configured via `.env.private`. +ALGOLIA_APP_ID="" +ALGOLIA_SEARCH_API_KEY="" + +BASE_URL="https://localhost.stage.edx.org:1991" +LICENSE_MANAGER_BASE_URL="https://license-manager.stage.edx.org" +ENTERPRISE_ACCESS_BASE_URL="https://enterprise-access.stage.edx.org" +ENTERPRISE_CATALOG_BASE_URL="https://enterprise-catalog.stage.edx.org" +ENTERPRISE_SUBSIDY_BASE_URL="https://enterprise-subsidy.stage.edx.org" +DISCOVERY_BASE_URL="https://discovery.stage.edx.org" +PLOTLY_SERVER_URL="https://enterprise-dash.edx.org/admin-analytics/" +SEGMENT_KEY="" +NEW_RELIC_AGENT_ID="" +NEW_RELIC_APP_ID="" +ALGOLIA_INDEX_NAME="enterprise_catalog_new" +FEATURE_PROGRAM_TITLES_FACET='true' +FEATURE_LANGUAGE_FACET='true' +FEATURE_CODE_MANAGEMENT='true' +FEATURE_REPORTING_CONFIGURATIONS='true' +FEATURE_ANALYTICS='true' +FEATURE_SUPPORT='true' +FEATURE_SAML_CONFIGURATION='' +FEATURE_CODE_VISIBILITY='true' +FEATURE_EXTERNAL_LMS_CONFIGURATION='' +FEATURE_BULK_ENROLLMENT='true' +FEATURE_FILE_ATTACHMENT='true' +FEATURE_SETTINGS_PAGE='true' +FEATURE_SETTINGS_PAGE_LMS_TAB='true' +FEATURE_SETTINGS_PAGE_APPEARANCE_TAB='true' +FEATURE_LEARNER_CREDIT_MANAGEMENT='true' +FEATURE_CONTENT_HIGHLIGHTS='true' +FEATURE_AUTH0_SELF_SERVICE_INTEGRATION='false' +HOTJAR_DEBUG='' +HOTJAR_APP_ID='' +FEATURE_SSO_SETTINGS_TAB='true' +FEATURE_API_CREDENTIALS_TAB='true' +FEATURE_PENDING_ENROLLMENT_ACTIONS='false' +# maintenance alert +IS_MAINTENANCE_ALERT_ENABLED='' +MAINTENANCE_ALERT_MESSAGE='edX is currently in a brief maintenance window. Functionality involving course enrollments is unavailable at this time, including enrollment and assignment. Please check back shortly to continue the learning journey.' +MAINTENANCE_ALERT_START_TIMESTAMP='' + +# Common + +LMS_BASE_URL="https://courses.stage.edx.org" +STUDIO_BASE_URL="https://studio.stage.edx.org" +DATA_API_BASE_URL="https://analyticsapi.stage.edx.org" +ECOMMERCE_BASE_URL="https://ecommerce.stage.edx.org" +DISCOVERY_API_BASE_URL="https://discovery.stage.edx.org" +PUBLISHER_BASE_URL="https://publisher.stage.edx.org/" +CREDENTIALS_BASE_URL="https://credentials.stage.edx.org" +ENTERPRISE_CATALOG_API_BASE_URL="https://enterprise-catalog.stage.edx.org" +INSIGHTS_BASE_URL="https://stage-insights.edx.org" +LEARNING_BASE_URL="https://learning.stage.edx.org" +LOGIN_URL="https://courses.stage.edx.org/login" +LOGOUT_URL="https://courses.stage.edx.org/logout" +MARKETING_SITE_BASE_URL="https://stage.edx.org" +ORDER_HISTORY_URL="https://orders.stage.edx.org/orders" +ENTERPRISE_MARKETING_URL="https://business.edx.org" +REGISTRAR_API_BASE_URL="https://registrar.stage.edx.org/api" +OPTIMIZELY_PROJECT_ID="1706490390" +ENTERPRISE_LEARNER_PORTAL_HOSTNAME="enterprise.stage.edx.org" +ENTERPRISE_LEARNER_PORTAL_URL="https://enterprise.stage.edx.org" +DEMOGRAPHICS_BASE_URL="https://demographics.stage.edx.org" +EXAMS_BASE_URL="https://edx-exams.stage.edx.org" +ACCOUNT_SETTINGS_URL="https://account.stage.edx.org" +ACCOUNT_PROFILE_URL="https://profile.stage.edx.org" +SUPPORT_URL="https://support.edx.org" +ENTERPRISE_SUPPORT_URL="https://business-support.edx.org/hc/en-us" +ENTERPRISE_SUPPORT_PROGRAM_OPTIMIZATION_URL="https://business.edx.org/hubfs/Onboarding%20and%20Engagement/Onboarding%20Assets/Admin%20Resources/Program%20Optimization.pdf?hsLang=en" +CONTACT_URL="https://courses.stage.edx.org/support/contact_us" +OPEN_SOURCE_URL="https://open.edx.org" +TERMS_OF_SERVICE_URL="https://stage.edx.org/edx-terms-service" +PRIVACY_POLICY_URL="https://stage.edx.org/edx-privacy-policy" +SPANISH_PRIVACY_POLICY_URL="https://stage.edx.org/es/edx-privacy-policy" +SEARCH_CATALOG_URL="https://stage.edx.org/search" +FACEBOOK_URL="https://www.facebook.com/edX" +TWITTER_URL="https://twitter.com/edXOnline" +YOU_TUBE_URL="https://www.youtube.com/user/edxonline" +LINKED_IN_URL="https://www.linkedin.com/school/edx/" +GOOGLE_PLUS_URL="https://plus.google.com/+edXOnline" +REDDIT_URL="https://www.reddit.com/r/edx" +APPLE_APP_STORE_URL="https://itunes.apple.com/us/app/edx/id945480667?mt=8" +GOOGLE_PLAY_URL="https://play.google.com/store/apps/details?id=org.edx.mobile" +ENTERPRISE_SUPPORT_REVOKE_LICENSE_URL="https://business-support.edx.org/hc/en-us/articles/4409008473495" +LANGUAGE_PREFERENCE_COOKIE_NAME="stage-edx-language-preference" +NEW_RELIC_ACCOUNT_ID="" +NEW_RELIC_LICENSE_KEY="" +NEW_RELIC_TRUST_KEY="" +ACCESS_TOKEN_COOKIE_NAME="stage-edx-jwt-cookie-header-payload" +USER_INFO_COOKIE_NAME="stage-edx-user-info" +REFRESH_ACCESS_TOKEN_ENDPOINT="https://courses.stage.edx.org/login_refresh" +CSRF_TOKEN_API_PATH="/csrf/api/v1/token" +SITE_NAME="edX" +FAVICON_URL="https://edx-cdn.org/v3/stage/favicon.ico" +LOGO_TRADEMARK_URL="https://edx-cdn.org/v3/stage/logo-trademark.svg" +LOGO_TRADEMARK_URL_PNG="https://edx-cdn.org/v3/stage/logo-trademark.png" +LOGO_TRADEMARK_URL_SVG="https://edx-cdn.org/v3/stage/logo-trademark.svg" +LOGO_URL="https://edx-cdn.org/v3/stage/logo.svg" +LOGO_URL_PNG="https://edx-cdn.org/v3/stage/logo.png" +LOGO_URL_SVG="https://edx-cdn.org/v3/stage/logo.svg" +LOGO_WHITE_URL="https://edx-cdn.org/v3/stage/logo-white.svg" +LOGO_WHITE_URL_PNG="https://edx-cdn.org/v3/stage/logo-white.png" +LOGO_WHITE_URL_SVG="https://edx-cdn.org/v3/stage/logo-white.svg" +LOGO_POWERED_BY_OPEN_EDX_URL="https://edx-cdn.org/v3/stage/open-edx-tag.png" +LOGO_POWERED_BY_OPEN_EDX_URL_PNG="https://edx-cdn.org/v3/stage/open-edx-tag.png" +LOGO_POWERED_BY_OPEN_EDX_URL_SVG="https://edx-cdn.org/v3/stage/open-edx-tag.svg" +HOTJAR_VERSION=6 +SESSION_COOKIE_DOMAIN=".stage.edx.org" + +# Cookie Policy Banner +COOKIE_POLICY_BANNER_VIEWED_NAME="edx-cookie-policy-viewed" +MARKETING_SITE_NAME="edx.org" diff --git a/package.json b/package.json index e41c5aa0dd..3d60221c8c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "postinstall": "patch-package", "install-theme": "npm install \"@edx/brand@${THEME}\" --no-save", "start": "fedx-scripts webpack-dev-server --progress", - "start:with-theme": "THEME=npm:@edx/brand-edx.org@latest npm run install-theme && fedx-scripts webpack-dev-server --progress", + "start:stage": "npm run start -- --config=webpack.dev-stage.config.js", + "start:with-theme": "THEME=npm:@edx/brand-edx.org@latest npm run install-theme && npm run start", + "start:stage:with-theme": "THEME=npm:@edx/brand-edx.org@latest npm run install-theme && npm run start:stage", "debug-test": "node --inspect-brk node_modules/.bin/jest --runInBand --coverage", "test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests --maxWorkers=50%", "test:watch": "npm run test -- --watch", diff --git a/webpack.dev-stage.config.js b/webpack.dev-stage.config.js new file mode 100644 index 0000000000..9ccd8b6f6c --- /dev/null +++ b/webpack.dev-stage.config.js @@ -0,0 +1,26 @@ +const { createConfig } = require('@edx/frontend-build'); +const path = require('path'); +const dotenv = require('dotenv'); + +/** + * Injects stage-specific env vars from .env.development-stage. + * + * Note: ideally, we could use the base config for `webpack-dev-stage` in + * `getBaseConfig` above, however it appears to have a bug so we have to + * manually load the stage-specific env vars ourselves for now. + * + * The .env.development-stage env vars must be loaded before the base + * config is created. + */ +dotenv.config({ + path: path.resolve(process.cwd(), '.env.development-stage'), +}); + +const config = createConfig('webpack-dev', { + devServer: { + allowedHosts: 'all', + server: 'https', + }, +}); + +module.exports = config; From c13e684a6298f714451f41f0632ad566a50de404 Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Wed, 6 Dec 2023 08:31:21 -0800 Subject: [PATCH 094/124] feat: implement cancel button for pending assignment (#1099) * feat: implement cancel button * fix: refactored * fix: test and chip logic * fix: updating prop type for error reason and fixed test after rebase * fix: prop type * fix: added unit tests and refactored code * fix: update failing test --- .../AssignmentDetailsTableCell.jsx | 5 +- .../AssignmentRowActionTableCell.jsx | 19 +- .../AssignmentStatusTableCell.jsx | 28 ++- .../AssignmentTableCancel.jsx | 34 +++- .../BudgetDetailPageWrapper.jsx | 34 +++- .../CancelAssignmentModal.jsx | 74 +++++++ .../PendingAssignmentCancelButton.jsx | 51 +++++ .../FailedCancellation.jsx | 56 ++++++ .../cards/NewAssignmentModalButton.jsx | 4 +- .../cards/tests/CourseCard.test.jsx | 10 +- .../data/hooks/index.js | 1 + .../data/hooks/useCancelContentAssignments.js | 42 ++++ .../useCancelContentAssignments.test.jsx | 141 ++++++++++++++ ...seSuccessfulAssignmentToastContextValue.js | 4 +- ...SuccessfulCancellationToastContextValue.js | 41 ++++ .../tests/CatalogSearchResults.test.jsx | 10 +- .../tests/BudgetDetailPage.test.jsx | 181 +++++++++++++++++- .../tests/BudgetDetailPageWrapper.test.jsx | 65 ++++++- .../services/EnterpriseAccessApiService.js | 11 ++ .../tests/EnterpriseAccessApiService.test.js | 12 ++ 20 files changed, 768 insertions(+), 55 deletions(-) create mode 100644 src/components/learner-credit-management/CancelAssignmentModal.jsx create mode 100644 src/components/learner-credit-management/PendingAssignmentCancelButton.jsx create mode 100644 src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx create mode 100644 src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js create mode 100644 src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx create mode 100644 src/components/learner-credit-management/data/hooks/useSuccessfulCancellationToastContextValue.js diff --git a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx index 83c8a0f26a..8ea26c70b2 100644 --- a/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentDetailsTableCell.jsx @@ -56,7 +56,10 @@ AssignmentDetailsTableCell.propTypes = { contentKey: PropTypes.string.isRequired, contentTitle: PropTypes.string, contentQuantity: PropTypes.number, - errorReason: PropTypes.string, + errorReason: PropTypes.shape({ + actionType: PropTypes.string, + errorReason: PropTypes.string, + }), learnerState: PropTypes.string, state: PropTypes.string, }).isRequired, diff --git a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx index eec4589cdd..74398b32c5 100644 --- a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import { Icon, IconButton, OverlayTrigger, Stack, Tooltip, } from '@edx/paragon'; -import { Mail, DoNotDisturbOn } from '@edx/paragon/icons'; +import { Mail } from '@edx/paragon/icons'; +import PendingAssignmentCancelButton from './PendingAssignmentCancelButton'; const AssignmentRowActionTableCell = ({ row }) => { const isLearnerStateWaiting = row.original.learnerState === 'waiting'; @@ -26,21 +27,7 @@ const AssignmentRowActionTableCell = ({ row }) => { /> )} - Cancel assignment} - > - console.log(`Canceling ${row.original.uuid}`)} - data-testid={`cancel-assignment-${row.original.uuid}`} - /> - + ); }; diff --git a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx index dbfc2c375a..63dffc765c 100644 --- a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx @@ -1,12 +1,12 @@ -import React from 'react'; -import PropTypes from 'prop-types'; import { Chip, } from '@edx/paragon'; -import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; -import WaitingForLearner from './assignments-status-chips/WaitingForLearner'; +import PropTypes from 'prop-types'; import FailedBadEmail from './assignments-status-chips/FailedBadEmail'; +import FailedCancellation from './assignments-status-chips/FailedCancellation'; import FailedSystem from './assignments-status-chips/FailedSystem'; +import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; +import WaitingForLearner from './assignments-status-chips/WaitingForLearner'; const AssignmentStatusTableCell = ({ row }) => { const { original } = row; @@ -36,13 +36,18 @@ const AssignmentStatusTableCell = ({ row }) => { if (learnerState === 'failed') { // Determine which failure chip to display based on the error reason. - if (errorReason === 'email_error') { - return ( - - ); + if (errorReason.actionType === 'notified') { + if (errorReason.errorReason === 'email_error') { + return ( + + ); + } + return ; } - return ; + if (errorReason.actionType === 'cancelled') { + return ; + } } // Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway. @@ -54,7 +59,10 @@ AssignmentStatusTableCell.propTypes = { original: PropTypes.shape({ learnerEmail: PropTypes.string, learnerState: PropTypes.string.isRequired, - errorReason: PropTypes.string, + errorReason: PropTypes.shape({ + actionType: PropTypes.string, + errorReason: PropTypes.string, + }), actions: PropTypes.arrayOf(PropTypes.shape({ actionType: PropTypes.string.isRequired, errorReason: PropTypes.string, diff --git a/src/components/learner-credit-management/AssignmentTableCancel.jsx b/src/components/learner-credit-management/AssignmentTableCancel.jsx index 37c3e8ef6d..cb45c1f4eb 100644 --- a/src/components/learner-credit-management/AssignmentTableCancel.jsx +++ b/src/components/learner-credit-management/AssignmentTableCancel.jsx @@ -2,13 +2,35 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from '@edx/paragon'; import { DoNotDisturbOn } from '@edx/paragon/icons'; +import CancelAssignmentModal from './CancelAssignmentModal'; +import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; -const AssignmentTableCancelAction = ({ selectedFlatRows, ...rest }) => ( - // eslint-disable-next-line no-console - -); +const AssignmentTableCancelAction = ({ selectedFlatRows }) => { + const assignmentUuids = selectedFlatRows.map(row => row.id); + const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; + const { + assignButtonState, + cancelContentAssignments, + close, + isOpen, + open, + } = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids); + + return ( + <> + + + + ); +}; AssignmentTableCancelAction.propTypes = { selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), diff --git a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx index b7ae27b754..0702441137 100644 --- a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { Container, Toast } from '@edx/paragon'; import Hero from '../Hero'; -import { useSuccessfulAssignmentToastContextValue } from './data'; +import { useSuccessfulAssignmentToastContextValue, useSuccessfulCancellationToastContextValue } from './data'; const PAGE_TITLE = 'Learner Credit Management'; @@ -20,22 +20,37 @@ const BudgetDetailPageWrapper = ({ const budgetDisplayName = subsidyAccessPolicy?.displayName || 'Overview'; const helmetPageTitle = budgetDisplayName ? `${budgetDisplayName} - ${PAGE_TITLE}` : PAGE_TITLE; - const successfulAssignmentToastContextValue = useSuccessfulAssignmentToastContextValue(); + const successfulAssignmentToast = useSuccessfulAssignmentToastContextValue(); + const successfulCancellationToast = useSuccessfulCancellationToastContextValue(); + const { isSuccessfulAssignmentAllocationToastOpen, successfulAssignmentAllocationToastMessage, closeToastForAssignmentAllocation, - } = successfulAssignmentToastContextValue; + } = successfulAssignmentToast; + + const { + isSuccessfulAssignmentCancellationToastOpen, + successfulAssignmentCancellationToastMessage, + closeToastForAssignmentCancellation, + } = successfulCancellationToast; + + const values = useMemo(() => ({ + successfulAssignmentToast, + successfulCancellationToast, + }), [successfulAssignmentToast, successfulCancellationToast]); return ( - + {includeHero && } {children} {/** - Successful assignment allocation Toast notification. It is rendered here to guarantee that the + Successful assignment allocation and cancellation Toast notifications. It is rendered here to guarantee that the Toast component will not be unmounted when the user programmatically navigates to the "Activity" tab, which will unmount the course cards that rendered the assignment modal. Thus, the Toast must be rendered within the component tree that's common to both the "Activity" and "Overview" tabs. @@ -46,6 +61,13 @@ const BudgetDetailPageWrapper = ({ > {successfulAssignmentAllocationToastMessage} + + + {successfulAssignmentCancellationToastMessage} + ); }; diff --git a/src/components/learner-credit-management/CancelAssignmentModal.jsx b/src/components/learner-credit-management/CancelAssignmentModal.jsx new file mode 100644 index 0000000000..2636fdb13d --- /dev/null +++ b/src/components/learner-credit-management/CancelAssignmentModal.jsx @@ -0,0 +1,74 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { + ActionRow, ModalDialog, StatefulButton, +} from '@edx/paragon'; +import { DoNotDisturbOn } from '@edx/paragon/icons'; +import { BudgetDetailPageContext } from './BudgetDetailPageWrapper'; + +const CancelAssignmentModal = ({ + assignButtonState, + cancelContentAssignments, + close, + isOpen, + uuidCount, +}) => { + const { + successfulCancellationToast: { displayToastForAssignmentCancellation }, + } = useContext(BudgetDetailPageContext); + + const handleOnClick = async () => { + await cancelContentAssignments(); + displayToastForAssignmentCancellation(uuidCount); + }; + + return ( + + + + Cancel assignment? + + + + +

    This action cannot be undone.

    +

    The learner will be notified that you have canceled the assignment. The funds associated with + this course assignment will move from "assigned" back to "available". +

    +
    + + + + Go back + 1 ? `Cancel assignments (${uuidCount})` : 'Cancel assignment', + pending: 'Canceling...', + complete: 'Canceled', + error: 'Try again', + }} + variant="danger" + state={assignButtonState} + onClick={handleOnClick} + /> + + +
    + ); +}; + +CancelAssignmentModal.propTypes = { + assignButtonState: PropTypes.string.isRequired, + cancelContentAssignments: PropTypes.func.isRequired, + close: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + uuidCount: PropTypes.number, +}; + +export default CancelAssignmentModal; diff --git a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx new file mode 100644 index 0000000000..692cdd4cfb --- /dev/null +++ b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Icon, IconButtonWithTooltip, +} from '@edx/paragon'; +import { DoNotDisturbOn } from '@edx/paragon/icons'; +import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; +import CancelAssignmentModal from './CancelAssignmentModal'; + +const PendingAssignmentCancelButton = ({ row }) => { + const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; + const { + assignButtonState, + cancelContentAssignments, + close, + isOpen, + open, + } = useCancelContentAssignments(row.original.assignmentConfiguration, [row.original.uuid]); + return ( + <> + + + + ); +}; + +PendingAssignmentCancelButton.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + assignmentConfiguration: PropTypes.string.isRequired, + learnerEmail: PropTypes.string, + uuid: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; + +export default PendingAssignmentCancelButton; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx new file mode 100644 index 0000000000..182e91df7b --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import { Chip, useToggle, Hyperlink } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; +import BaseModalPopup from './BaseModalPopup'; + +const FailedCancellation = () => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = useState(null); + + return ( + <> + + Failed: Cancellation + + + + Failed: Cancellation + + +

    This assignment was not canceled. Something went wrong behind the scenes.

    +
    +

    Suggested resolution steps

    +
      +
    • + Wait and try to cancel this assignment again later +
    • +
    • + If the issue continues, contact customer support. +
    • +
    • + Get more troubleshooting help at{' '} + + Help Center: Course Assignments + +
    • +
    +
    +
    +
    + + ); +}; + +export default FailedCancellation; diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 1c819cffeb..e01816489e 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -41,7 +41,9 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const [canAllocateAssignments, setCanAllocateAssignments] = useState(false); const [assignButtonState, setAssignButtonState] = useState('default'); const [createAssignmentsErrorReason, setCreateAssignmentsErrorReason] = useState(); - const { displayToastForAssignmentAllocation } = useContext(BudgetDetailPageContext); + const { + successfulAssignmentToast: { displayToastForAssignmentAllocation }, + } = useContext(BudgetDetailPageContext); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const { subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 73f6d19784..7840ecad59 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -107,10 +107,12 @@ const mockLearnerEmails = ['hello@example.com', 'world@example.com', 'dinesh@exa const mockDisplaySuccessfulAssignmentToast = jest.fn(); const defaultBudgetDetailPageContextValue = { - isSuccessfulAssignmentAllocationToastOpen: false, - totalLearnersAssigned: undefined, - displayToastForAssignmentAllocation: mockDisplaySuccessfulAssignmentToast, - closeToastForAssignmentAllocation: jest.fn(), + successfulAssignmentToast: { + isSuccessfulAssignmentAllocationToastOpen: false, + totalLearnersAssigned: undefined, + displayToastForAssignmentAllocation: mockDisplaySuccessfulAssignmentToast, + closeToastForAssignmentAllocation: jest.fn(), + }, }; const CourseCardWrapper = ({ diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index 61731bd38e..400f754b57 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -8,3 +8,4 @@ export { default as usePathToCatalogTab } from './usePathToCatalogTab'; export { default as useBudgetDetailActivityOverview } from './useBudgetDetailActivityOverview'; export { default as useIsLargeOrGreater } from './useIsLargeOrGreater'; export { default as useSuccessfulAssignmentToastContextValue } from './useSuccessfulAssignmentToastContextValue'; +export { default as useSuccessfulCancellationToastContextValue } from './useSuccessfulCancellationToastContextValue'; diff --git a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js new file mode 100644 index 0000000000..8b9bec8d6f --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js @@ -0,0 +1,42 @@ +import { useCallback, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { logError } from '@edx/frontend-platform/logging'; +import { useToggle } from '@edx/paragon'; + +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import { learnerCreditManagementQueryKeys } from '../constants'; +import useBudgetId from './useBudgetId'; + +const useCancelContentAssignments = ( + assignmentConfigurationUuid, + assignmentUuids, +) => { + const [isOpen, open, close] = useToggle(false); + const [assignButtonState, setAssignButtonState] = useState('default'); + const queryClient = useQueryClient(); + const { subsidyAccessPolicyId } = useBudgetId(); + + const cancelContentAssignments = useCallback(async () => { + setAssignButtonState('pending'); + try { + await EnterpriseAccessApiService.cancelContentAssignments(assignmentConfigurationUuid, assignmentUuids); + setAssignButtonState('complete'); + queryClient.invalidateQueries({ + queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), + }); + } catch (err) { + logError(err); + setAssignButtonState('error'); + } + }, [assignmentConfigurationUuid, assignmentUuids, queryClient, subsidyAccessPolicyId]); + + return { + assignButtonState, + cancelContentAssignments, + close, + isOpen, + open, + }; +}; + +export default useCancelContentAssignments; diff --git a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx new file mode 100644 index 0000000000..4044003fef --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx @@ -0,0 +1,141 @@ +import { useParams } from 'react-router-dom'; +import { renderHook } from '@testing-library/react-hooks/dom'; +import { act, waitFor } from '@testing-library/react'; +import { QueryClientProvider } from '@tanstack/react-query'; + +import { logError } from '@edx/frontend-platform/logging'; + +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import useCancelContentAssignments from './useCancelContentAssignments'; +import { queryClient } from '../../../test/testUtils'; + +const TEST_ASSIGNMENT_CONFIGURATION_UUID = 'test-assignment-configuration-uuid'; +const TEST_PENDING_ASSIGNMENT_UUID_1 = 'test-pending-assignment-uuid_1'; +const TEST_PENDING_ASSIGNMENT_UUID_2 = 'test-pending-assignment-uuid_2'; + +const wrapper = ({ children }) => ( + {children} +); + +jest.mock('../../../../data/services/EnterpriseAccessApiService'); +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})); + +describe('useCancelContentAssignments', () => { + beforeEach(() => { + useParams.mockReturnValue({ + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should send a post request to cancel a single pending assignment', async () => { + EnterpriseAccessApiService.cancelContentAssignments.mockResolvedValueOnce({ status: 200 }); + const { result } = renderHook( + () => useCancelContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + TEST_PENDING_ASSIGNMENT_UUID_1, + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + assignButtonState: 'default', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => act(() => result.current.cancelContentAssignments())); + expect( + EnterpriseAccessApiService.cancelContentAssignments, + ).toHaveBeenCalled(); + expect(logError).toBeCalledTimes(0); + + expect(result.current).toEqual({ + assignButtonState: 'complete', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); + + it('should send a post request to cancel multiple pending assignments', async () => { + EnterpriseAccessApiService.cancelContentAssignments.mockResolvedValueOnce({ status: 200 }); + const { result } = renderHook( + () => useCancelContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + [TEST_PENDING_ASSIGNMENT_UUID_1, TEST_PENDING_ASSIGNMENT_UUID_2], + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + assignButtonState: 'default', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => act(() => result.current.cancelContentAssignments())); + expect( + EnterpriseAccessApiService.cancelContentAssignments, + ).toHaveBeenCalled(); + expect(logError).toBeCalledTimes(0); + + expect(result.current).toEqual({ + assignButtonState: 'complete', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); + + it('should handle assignment cancellation error', async () => { + const error = new Error('An error occurred'); + EnterpriseAccessApiService.cancelContentAssignments.mockRejectedValueOnce(error); + const { result } = renderHook( + () => useCancelContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + [TEST_PENDING_ASSIGNMENT_UUID_1, TEST_PENDING_ASSIGNMENT_UUID_2], + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + assignButtonState: 'default', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => act(() => result.current.cancelContentAssignments())); + + expect( + EnterpriseAccessApiService.cancelContentAssignments, + ).toHaveBeenCalled(); + expect(logError).toBeCalledTimes(1); + + expect(result.current).toEqual({ + assignButtonState: 'error', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); +}); diff --git a/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js b/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js index b86113f604..1bcee3dbf7 100644 --- a/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js +++ b/src/components/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js @@ -26,7 +26,7 @@ const useSuccessfulAssignmentToastContextValue = () => { } const successfulAssignmentAllocationToastMessage = toastMessages.join(' '); - const successfulAssignmentToastContextValue = useMemo(() => ({ + const successfulAssignmentToast = useMemo(() => ({ isSuccessfulAssignmentAllocationToastOpen: isToastOpen, displayToastForAssignmentAllocation: handleDisplayToast, closeToastForAssignmentAllocation: handleCloseToast, @@ -42,7 +42,7 @@ const useSuccessfulAssignmentToastContextValue = () => { successfulAssignmentAllocationToastMessage, ]); - return successfulAssignmentToastContextValue; + return successfulAssignmentToast; }; export default useSuccessfulAssignmentToastContextValue; diff --git a/src/components/learner-credit-management/data/hooks/useSuccessfulCancellationToastContextValue.js b/src/components/learner-credit-management/data/hooks/useSuccessfulCancellationToastContextValue.js new file mode 100644 index 0000000000..a2bdf89bba --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useSuccessfulCancellationToastContextValue.js @@ -0,0 +1,41 @@ +import { useCallback, useMemo, useState } from 'react'; + +const generateSuccessCancelMessage = (assignmentUuidCount) => { + if (assignmentUuidCount > 1) { + return `Assignments canceled (${assignmentUuidCount})`; + } + + return 'Assignment canceled'; +}; + +const useSuccessfulCancellationToastContextValue = () => { + const [isToastOpen, setIsToastOpen] = useState(false); + const [assignmentUuidCount, setAssignmentUuidCount] = useState(); + + const handleDisplayToast = useCallback((assignmentUuids) => { + setIsToastOpen(true); + setAssignmentUuidCount(assignmentUuids); + }, []); + + const handleCloseToast = useCallback(() => { + setIsToastOpen(false); + }, []); + + const successfulAssignmentCancellationToastMessage = generateSuccessCancelMessage(assignmentUuidCount); + + const successfulCancellationToastContextValue = useMemo(() => ({ + isSuccessfulAssignmentCancellationToastOpen: isToastOpen, + displayToastForAssignmentCancellation: handleDisplayToast, + closeToastForAssignmentCancellation: handleCloseToast, + successfulAssignmentCancellationToastMessage, + }), [ + isToastOpen, + handleDisplayToast, + handleCloseToast, + successfulAssignmentCancellationToastMessage, + ]); + + return successfulCancellationToastContextValue; +}; + +export default useSuccessfulCancellationToastContextValue; diff --git a/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx index d46d96dec2..c05f643881 100644 --- a/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx +++ b/src/components/learner-credit-management/search/tests/CatalogSearchResults.test.jsx @@ -175,10 +175,12 @@ describe('Main Catalogs view works as expected', () => { test('all courses rendered when search results available', async () => { const budgetDetailPageContextValue = { - isSuccessfulAssignmentAllocationToastOpen: false, - totalLearnersAssigned: undefined, - displayToastForAssignmentAllocation: jest.fn(), - closeToastForAssignmentAllocation: jest.fn(), + successfulAssignmentToast: { + isSuccessfulAssignmentAllocationToastOpen: false, + totalLearnersAssigned: undefined, + displayToastForAssignmentAllocation: jest.fn(), + closeToastForAssignmentAllocation: jest.fn(), + }, }; renderWithRouter( diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 5138d10080..ac73a03d64 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -12,6 +12,7 @@ import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterp import { act } from 'react-dom/test-utils'; import { v4 as uuidv4 } from 'uuid'; import { faker } from '@faker-js/faker'; +import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import BudgetDetailPage from '../BudgetDetailPage'; import { @@ -51,8 +52,11 @@ jest.mock('../data', () => ({ useSubsidyAccessPolicy: jest.fn(), useBudgetDetailActivityOverview: jest.fn(), useIsLargeOrGreater: jest.fn().mockReturnValue(true), + useCancelContentAssignments: jest.fn(), })); +jest.mock('../../../data/services/EnterpriseAccessApiService'); + const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); const enterpriseSlug = 'test-enterprise'; @@ -141,6 +145,11 @@ const mockEnrollmentTransactionWithReversal = { reversal: mockEnrollmentTransactionReversal, }; +const mockFailedCancelledLearnerAction = { + actionType: 'cancelled', + completedAt: null, + errorReason: 'email_error', +}; const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; @@ -801,7 +810,10 @@ describe('', () => { expectedModalPopupHeading: 'Failed: Bad email', expectedModalPopupContent: `This course assignment failed because a notification to ${mockLearnerEmail} could not be sent.`, actions: [mockSuccessfulLinkedLearnerAction, mockFailedNotifiedAction], - errorReason: 'email_error', + errorReason: { + errorReason: 'email_error', + actionType: 'notified', + }, }, { learnerState: 'failed', @@ -810,7 +822,10 @@ describe('', () => { expectedModalPopupHeading: 'Failed: Bad email', expectedModalPopupContent: 'This course assignment failed because a notification to the learner could not be sent.', actions: [mockSuccessfulLinkedLearnerAction, mockFailedNotifiedAction], - errorReason: 'email_error', + errorReason: { + errorReason: 'email_error', + actionType: 'notified', + }, }, { learnerState: 'failed', @@ -819,7 +834,34 @@ describe('', () => { expectedModalPopupHeading: 'Failed: System', expectedModalPopupContent: 'Something went wrong behind the scenes.', actions: [mockFailedLinkedLearnerAction], - errorReason: 'internal_api_error', + errorReason: { + errorReason: 'internal_api_error', + actionType: 'notified', + }, + }, + { + learnerState: 'failed', + hasLearnerEmail: true, + expectedChipStatus: 'Failed: Cancellation', + expectedModalPopupHeading: 'Failed: Cancellation', + expectedModalPopupContent: 'Something went wrong behind the scenes.', + actions: [mockFailedCancelledLearnerAction], + errorReason: { + errorReason: 'email_error', + actionType: 'cancelled', + }, + }, + { + learnerState: 'failed', + hasLearnerEmail: true, + expectedChipStatus: 'Failed: Cancellation', + expectedModalPopupHeading: 'Failed: Cancellation', + expectedModalPopupContent: 'Something went wrong behind the scenes.', + actions: [mockFailedCancelledLearnerAction], + errorReason: { + errorReason: 'internal_api_error', + actionType: 'cancelled', + }, }, ])('renders correct status chips with assigned table data (%s)', ({ learnerState, @@ -1158,4 +1200,137 @@ describe('', () => { expect(remindButton).toBeDisabled(); } }); + + it('cancels assignments in bulk', async () => { + EnterpriseAccessApiService.cancelContentAssignments.mockResolvedValueOnce({ status: 200 }); + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useBudgetRedemptions.mockReturnValue({ + isLoading: false, + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 2, + results: [ + { + uuid: 'test-uuid1', + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + }, + { + uuid: 'test-uuid2', + contentKey: mockCourseKey, + contentQuantity: -29900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-11-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + }, + ], + learnerStateCounts: [ + { learnerState: 'waiting', count: 1 }, + { learnerState: 'waiting', count: 1 }, + ], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + renderWithRouter(); + const cancelRowAction = screen.getByTitle('Toggle All Current Page Rows Selected'); + expect(cancelRowAction).toBeInTheDocument(); + userEvent.click(cancelRowAction); + const cancelBulkActionButton = screen.getByText('Cancel (2)'); + expect(cancelBulkActionButton).toBeInTheDocument(); + userEvent.click(cancelBulkActionButton); + const modalDialog = screen.getByRole('dialog'); + expect(modalDialog).toBeInTheDocument(); + const cancelDialogButton = getButtonElement('Cancel assignments (2)'); + userEvent.click(cancelDialogButton); + expect( + EnterpriseAccessApiService.cancelContentAssignments, + ).toHaveBeenCalled(); + await waitFor( + () => expect(screen.getByText('Assignments canceled (2)')).toBeInTheDocument(), + ); + }); + + it('cancels a single assignment', async () => { + EnterpriseAccessApiService.cancelContentAssignments.mockResolvedValueOnce({ status: 200 }); + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useBudgetRedemptions.mockReturnValue({ + isLoading: false, + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [ + { + uuid: 'test-uuid', + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + }, + ], + learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + renderWithRouter(); + const cancelIconButton = screen.getByTestId('cancel-assignment-test-uuid'); + expect(cancelIconButton).toBeInTheDocument(); + userEvent.click(cancelIconButton); + const modalDialog = screen.getByRole('dialog'); + expect(modalDialog).toBeInTheDocument(); + const cancelDialogButton = getButtonElement('Cancel assignment'); + userEvent.click(cancelDialogButton); + await waitFor( + () => expect(screen.getByText('Assignment canceled')).toBeInTheDocument(), + ); + }); }); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx index e219ee25b2..a5d07d93ae 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx @@ -96,8 +96,10 @@ describe('', () => { }) => { const ToastContextController = () => { const { - displayToastForAssignmentAllocation, - closeToastForAssignmentAllocation, + successfulAssignmentToast: { + displayToastForAssignmentAllocation, + closeToastForAssignmentAllocation, + }, } = useContext(BudgetDetailPageContext); const handleDisplayToast = () => { @@ -143,4 +145,63 @@ describe('', () => { expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); }); }); + + it.each([ + { + assignmentUUIDs: 1, + }, + { + assignmentUUIDs: 2, + }, + ])('should render Toast notification for successful assignment cancellation (%s)', async ({ + assignmentUUIDs, + }) => { + const ToastContextController = () => { + const { + successfulCancellationToast: { + displayToastForAssignmentCancellation, + closeToastForAssignmentCancellation, + }, + } = useContext(BudgetDetailPageContext); + + const handleDisplayToast = () => { + displayToastForAssignmentCancellation(assignmentUUIDs); + }; + + const handleCloseToast = () => { + closeToastForAssignmentCancellation(); + }; + + return ( +
    + + +
    + ); + }; + render(); + + const toastMessages = []; + if (assignmentUUIDs > 1) { + toastMessages.push(`Assignments canceled (${assignmentUUIDs})`); + } + if (assignmentUUIDs === 1) { + toastMessages.push('Assignment canceled'); + } + const expectedToastMessage = toastMessages.join(' '); + + // Open Toast notification + userEvent.click(getButtonElement('Open Toast')); + + // Verify Toast notification is rendered + expect(screen.getByText(expectedToastMessage)).toBeInTheDocument(); + + // Close Toast notification + userEvent.click(getButtonElement('Close Toast')); + + // Verify Toast notification is no longer rendered + await waitFor(() => { + expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index 94e4fcae95..9ac00c5a52 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -172,6 +172,17 @@ class EnterpriseAccessApiService { return EnterpriseAccessApiService.apiClient().get(url); } + /** + * Cancel content assignments for a specific AssignmentConfiguration. + */ + static cancelContentAssignments(assignmentConfigurationUUID, assignmentUuids) { + const options = { + assignment_uuids: assignmentUuids, + }; + const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/cancel/`; + return EnterpriseAccessApiService.apiClient().post(url, options); + } + /** * Retrieve a specific subsidy access policy. * @param {string} subsidyAccessPolicyUUID The UUID of the subsidy access policy to retrieve. diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index 7317e14639..f384281752 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -19,6 +19,7 @@ const mockLicenseRequestUUID = 'test-license-request-uuid'; const mockCouponCodeRequestUUID = 'test-coupon-code-request-uuid'; const mockAssignmentConfigurationUUID = 'test-assignment-configuration-uuid'; const mockSubsidyAccessPolicyUUID = 'test-subsidy-access-policy-uuid'; +const mockAssignmentUUIDs = ['test-assignment-uuid1', 'test-assignment-uuid-2']; describe('EnterpriseAccessApiService', () => { beforeEach(() => { @@ -188,4 +189,15 @@ describe('EnterpriseAccessApiService', () => { payload, ); }); + + test('cancelContentAssignments calls enterprise-access cancel POST API to cancel assignments', () => { + const options = { + assignment_uuids: mockAssignmentUUIDs, + }; + EnterpriseAccessApiService.cancelContentAssignments(mockAssignmentConfigurationUUID, mockAssignmentUUIDs); + expect(axios.post).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/cancel/`, + options, + ); + }); }); From b990fec8bce87097cad839ea6adeb88843aebaf4 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Wed, 6 Dec 2023 13:55:08 -0500 Subject: [PATCH 095/124] feat: display only active budgets from subsidy-access-policy (#1122) --- src/data/services/EnterpriseAccessApiService.js | 1 + src/data/services/tests/EnterpriseAccessApiService.test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index 9ac00c5a52..cb7e0da9a5 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -167,6 +167,7 @@ class EnterpriseAccessApiService { static listSubsidyAccessPolicies(enterpriseCustomerId) { const queryParams = new URLSearchParams({ enterprise_customer_uuid: enterpriseCustomerId, + active: true, }); const url = `${EnterpriseAccessApiService.baseUrl}/subsidy-access-policies/?${queryParams.toString()}`; return EnterpriseAccessApiService.apiClient().get(url); diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index f384281752..51fe177d49 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -166,7 +166,7 @@ describe('EnterpriseAccessApiService', () => { test('listSubsidyAccessPolicies calls enterprise-access to fetch subsidy access policies', () => { EnterpriseAccessApiService.listSubsidyAccessPolicies(mockEnterpriseUUID); expect(axios.get).toBeCalledWith( - `${enterpriseAccessBaseUrl}/api/v1/subsidy-access-policies/?enterprise_customer_uuid=${mockEnterpriseUUID}`, + `${enterpriseAccessBaseUrl}/api/v1/subsidy-access-policies/?enterprise_customer_uuid=${mockEnterpriseUUID}&active=true`, ); }); From 4f335005d932cab4826766578ceea0cb7dc08820 Mon Sep 17 00:00:00 2001 From: Marlon Keating <322346+marlonkeating@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:15:48 -0800 Subject: [PATCH 096/124] feat: Persist SSO Config 'authorized' checkbox state (#1121) --- .../SettingsSSOTab/SSOFormWorkflowConfig.tsx | 37 +++++++++++-------- .../steps/NewSSOConfigAuthorizeStep.tsx | 10 ++--- .../tests/NewSSOConfigForm.test.jsx | 6 ++- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx index ca7dd08e07..218811ac3e 100644 --- a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx +++ b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx @@ -1,5 +1,6 @@ import omit from 'lodash/omit'; +import { AxiosError } from 'axios'; import type { FormWorkflowHandlerArgs, FormWorkflowStep } from '../../forms/FormWorkflow'; import SSOConfigConnectStep, { validations as SSOConfigConnectStepValidations } from './steps/NewSSOConfigConnectStep'; import SSOConfigConfigureStep, { validations as SSOConfigConfigureStepValidations } from './steps/NewSSOConfigConfigureStep'; @@ -8,7 +9,6 @@ import SSOConfigConfirmStep from './steps/NewSSOConfigConfirmStep'; import LmsApiService from '../../../data/services/LmsApiService'; import handleErrors from '../utils'; import { snakeCaseDict } from '../../../utils'; -import { AxiosError } from 'axios'; import { INVALID_IDP_METADATA_ERROR, RECORD_UNDER_CONFIGURATIONS_ERROR } from '../data/constants'; type SSOConfigSnakeCase = { @@ -40,6 +40,7 @@ type SSOConfigSnakeCase = { oauth_user_id: string, sp_metadata_url?: string, record?: object, + marked_authorized: boolean }; export type SSOConfigCamelCase = { @@ -70,7 +71,8 @@ export type SSOConfigCamelCase = { sapsfPrivateKey: string, odataClientId: string, oauthUserId: string, - spMetadataUrl?: string + spMetadataUrl?: string, + markedAuthorized: boolean }; type SSOConfigFormControlVariables = { @@ -81,13 +83,6 @@ type SSOConfigFormControlVariables = { type SSOConfigFormContextData = SSOConfigCamelCase & SSOConfigFormControlVariables; export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { - const placeHolderButton = (buttonName?: string) => () => ({ - buttonText: buttonName || 'Next', - opensNewWindow: false, - onClick: () => { }, - preventDefaultErrorModal: false, - }); - const advanceConnectStep = async ({ formFields, errHandler, @@ -98,7 +93,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { const sanitizeAndCopyFormFields = (formFields: SSOConfigSnakeCase) => { const copiedFormFields = { ...formFields }; - return omit(copiedFormFields, ['record', 'sp_metadata_url', 'submitted_at', 'configured_at','validated_at']); + return omit(copiedFormFields, ['record', 'sp_metadata_url', 'submitted_at', 'configured_at', 'validated_at']); }; const saveChanges = async ({ @@ -117,7 +112,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { let updatedFormFields: SSOConfigCamelCase = omit(formFields, ['idpConnectOption', 'spMetadataUrl', 'isPendingConfiguration']); updatedFormFields.enterpriseCustomer = enterpriseId; const submittedFormFields: SSOConfigSnakeCase = snakeCaseDict(updatedFormFields) as SSOConfigSnakeCase; - let copiedFormFields = sanitizeAndCopyFormFields(submittedFormFields); + const copiedFormFields = sanitizeAndCopyFormFields(submittedFormFields); if (copiedFormFields?.uuid) { try { const updateResponse = await LmsApiService.updateEnterpriseSsoOrchestrationRecord( @@ -127,9 +122,9 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { updatedFormFields = updateResponse.data; } catch (error: AxiosError | any) { err = handleErrors(error); - if (error.message?.includes("Must provide valid IDP metadata url")) { + if (error.message?.includes('Must provide valid IDP metadata url')) { errHandler?.(INVALID_IDP_METADATA_ERROR); - } else if (error.message?.includes("Record has already been submitted for configuration.")) { + } else if (error.message?.includes('Record has already been submitted for configuration.')) { errHandler?.(RECORD_UNDER_CONFIGURATIONS_ERROR); } else { setConfigureError(error); @@ -142,7 +137,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { updatedFormFields.spMetadataUrl = createResponse.data.sp_metadata_url; } catch (error: AxiosError | any) { err = handleErrors(error); - if (error.message?.includes("Must provide valid IDP metadata url")) { + if (error.message?.includes('Must provide valid IDP metadata url')) { errHandler?.(INVALID_IDP_METADATA_ERROR); } else { setConfigureError(error); @@ -184,7 +179,12 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { formComponent: SSOConfigAuthorizeStep, validations: SSOConfigAuthorizeStepValidations, stepName: 'Authorize', - nextButtonConfig: placeHolderButton(), + nextButtonConfig: () => ({ + buttonText: 'Next', + opensNewWindow: false, + onClick: saveChanges, + preventDefaultErrorModal: false, + }), showBackButton: true, showCancelButton: false, }, { @@ -192,7 +192,12 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { formComponent: SSOConfigConfirmStep, validations: [], stepName: 'Confirm and Test', - nextButtonConfig: placeHolderButton('Finish'), + nextButtonConfig: () => ({ + buttonText: 'Finish', + opensNewWindow: false, + onClick: () => {}, + preventDefaultErrorModal: false, + }), showBackButton: true, showCancelButton: false, }, diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx index dd640f2450..88d280c983 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigAuthorizeStep.tsx @@ -1,21 +1,20 @@ import React, { useContext } from 'react'; import { useParams } from 'react-router-dom'; import { - Alert, Form, Hyperlink, Button, Row, + Alert, Hyperlink, Button, Row, } from '@edx/paragon'; import { Info, Download } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import { createSAMLURLs } from '../utils'; import { SSOConfigContext } from '../SSOConfigContext'; -import { setFormFieldAction } from '../../../forms/data/actions'; import { FormFieldValidation, useFormContext } from '../../../forms/FormContext'; import ValidatedFormCheckbox from '../../../forms/ValidatedFormCheckbox'; export const validations: FormFieldValidation[] = [ { - formFieldId: 'confirmAuthorizedEdxServiceProvider', + formFieldId: 'markedAuthorized', validator: (fields) => { - const ret = !fields.confirmAuthorizedEdxServiceProvider && 'Please verify authorization of edX as a Service Provider.'; + const ret = !fields.markedAuthorized && 'Please verify authorization of edX as a Service Provider.'; return ret; }, }, @@ -40,7 +39,6 @@ const SSOConfigAuthorizeStep = () => { ssoState, } = useContext(SSOConfigContext); const { - dispatch, formFields, } = useFormContext(); const { enterpriseSlug } = useParams(); @@ -80,7 +78,7 @@ const SSOConfigAuthorizeStep = () => {


    Return to this window and check the box once complete

    - + I have authorized edX as a Service Provider diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx index 084157a67b..3f08d7cbe5 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigForm.test.jsx @@ -421,7 +421,10 @@ describe('SAML Config Tab', () => { test('navigate through new non-SAP sso workflow', async () => { setupNewSSOStepper(); const mockCreateEnterpriseSsoOrchestrationRecord = jest.spyOn(LmsApiService, 'createEnterpriseSsoOrchestrationRecord'); - mockCreateEnterpriseSsoOrchestrationRecord.mockResolvedValue({ data: { record: 'fakeuuid', sp_metadata_url: 'https://fake.url' } }); + const mockUpdateEnterpriseSsoOrchestrationRecord = jest.spyOn(LmsApiService, 'updateEnterpriseSsoOrchestrationRecord'); + const mockReturnData = { data: { record: 'fakeuuid', sp_metadata_url: 'https://fake.url' } }; + mockCreateEnterpriseSsoOrchestrationRecord.mockResolvedValue(mockReturnData); + mockUpdateEnterpriseSsoOrchestrationRecord.mockResolvedValue(mockReturnData); jest.spyOn(Router, 'useParams').mockReturnValue({ enterpriseSlug: 'testslug' }); // Connect Step await waitFor(() => { @@ -504,7 +507,6 @@ describe('SAML Config Tab', () => { }, []); screen.queryByText(testMetadataUrl); }); - // TODO: Test case where we go SAP route test('navigate through new SAP sso workflow', async () => { setupNewSSOStepper(); const mockCreateEnterpriseSsoOrchestrationRecord = jest.spyOn(LmsApiService, 'createEnterpriseSsoOrchestrationRecord'); From d1252adb1955c9a82ce425dd7f269b5806d65c08 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Wed, 6 Dec 2023 16:49:32 -0500 Subject: [PATCH 097/124] fix: ensure emails with uppercase letter passes duplicate email validation (#1124) --- .../cards/data/utils.js | 22 +++++++++++++++++-- .../cards/tests/CourseCard.test.jsx | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/learner-credit-management/cards/data/utils.js b/src/components/learner-credit-management/cards/data/utils.js index 8dd63d436c..63136a412e 100644 --- a/src/components/learner-credit-management/cards/data/utils.js +++ b/src/components/learner-credit-management/cards/data/utils.js @@ -52,8 +52,26 @@ export const isEmailAddressesInputValueValid = ({ const remainingBalanceAfterAssignment = remainingBalance - totalAssignmentCost; const hasEnoughBalanceForAssigment = remainingBalanceAfterAssignment >= 0; - const invalidEmails = learnerEmails.filter((email) => !isEmail(email)); - const duplicateEmails = learnerEmails.filter((email, index) => learnerEmails.indexOf(email.toLowerCase()) !== index); + const lowerCasedEmails = []; + const invalidEmails = []; + const duplicateEmails = []; + + learnerEmails.forEach((email) => { + const lowerCasedEmail = email.toLowerCase(); + + // Validate the email address + if (!isEmail(email)) { + invalidEmails.push(email); + } + + // Check for duplicates (case-insensitive) + if (lowerCasedEmails.includes(lowerCasedEmail)) { + duplicateEmails.push(email); + } + + // Add to list of lower-cased emails already handled + lowerCasedEmails.push(lowerCasedEmail); + }); const isValidInput = invalidEmails.length === 0 && duplicateEmails.length === 0; const canAllocate = learnerEmailsCount > 0 && hasEnoughBalanceForAssigment && isValidInput; diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 7840ecad59..517f14fbd5 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -548,7 +548,7 @@ describe('Course card works as expected', () => { expectedValidationMessage: 'The total assignment cost exceeds your available Learner Credit budget balance of $100. Please remove learners and try again.', }, { - learnerEmails: ['a@a.com', 'b@b.com', 'c@c.com'], + learnerEmails: ['a@a.com', 'B@b.com', 'c@c.com'], spendAvailableUsd: 1000, expectedValidationMessage: undefined, // no validation error }, From 2296c6c4e0f40842af3d58224bd9c55765dcc483 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Wed, 6 Dec 2023 17:17:37 -0500 Subject: [PATCH 098/124] fix: ensure pending balance is shown on budget cards even if 0 (#1120) --- .../EnterpriseSubsidiesContext/data/hooks.js | 2 ++ .../data/tests/hooks.test.jsx | 1 + .../learner-credit-management/BudgetCard.jsx | 2 ++ .../learner-credit-management/SubBudgetCard.jsx | 4 +++- .../data/hooks/useSubsidyAccessPolicy.js | 10 ++-------- .../tests/BudgetCard.test.jsx | 1 + src/utils.js | 12 ++++++++++++ 7 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index c576a19541..d30b9d2e3f 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -12,6 +12,7 @@ import SubsidyApiService from '../../../data/services/EnterpriseSubsidyApiServic import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys } from '../../learner-credit-management/data'; +import { isAssignableSubsidyAccessPolicyType } from '../../../utils'; dayjs.extend(isBetween); @@ -74,6 +75,7 @@ async function fetchEnterpriseBudgets({ spent: result.aggregates.amountRedeemedUsd, pending: result.aggregates.amountAllocatedUsd, }, + isAssignable: isAssignableSubsidyAccessPolicyType(result), }); }); enterpriseSubsidyResults?.forEach((result) => { diff --git a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx index 4a8b09fa08..6fc0c3bb38 100644 --- a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx +++ b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.jsx @@ -243,6 +243,7 @@ describe('useEnterpriseBudgets', () => { end: mockBudgetEnd, isCurrent: true, source: BUDGET_TYPES.policy, + isAssignable: false, aggregates: { available: 700, spent: 200, diff --git a/src/components/learner-credit-management/BudgetCard.jsx b/src/components/learner-credit-management/BudgetCard.jsx index db5478548c..19d70f79a1 100644 --- a/src/components/learner-credit-management/BudgetCard.jsx +++ b/src/components/learner-credit-management/BudgetCard.jsx @@ -40,6 +40,7 @@ const BudgetCard = ({ pending={budget.aggregates.pending} displayName={budget.name} enterpriseSlug={enterpriseSlug} + isAssignable={budget.isAssignable} /> ); } @@ -94,6 +95,7 @@ BudgetCard.propTypes = { spent: PropTypes.number.isRequired, pending: PropTypes.number, }), + isAssignable: PropTypes.bool, }).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index 1a6125bdc4..acd5a33560 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -31,6 +31,7 @@ const SubBudgetCard = ({ displayName, enterpriseSlug, isLoading, + isAssignable, }) => { const { isFetchingBudgets } = useContext(EnterpriseSubsidiesContext); const budgetLabel = getBudgetStatus(start, end); @@ -81,7 +82,7 @@ const SubBudgetCard = ({ {isFetchingBudgets ? : formatPrice(available)} - {pending > 0 && ( + {isAssignable && (
    Pending
    @@ -128,6 +129,7 @@ SubBudgetCard.propTypes = { available: PropTypes.number, pending: PropTypes.number, displayName: PropTypes.string, + isAssignable: PropTypes.bool, }; export default SubBudgetCard; diff --git a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js index 7058cdcb1a..620c66d3dd 100644 --- a/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js +++ b/src/components/learner-credit-management/data/hooks/useSubsidyAccessPolicy.js @@ -3,13 +3,7 @@ import { camelCaseObject } from '@edx/frontend-platform/utils'; import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys } from '../constants'; - -const determineBudgetAssignability = (subsidyAccessPolicy) => { - const policyType = subsidyAccessPolicy?.policyType; - const isAssignable = !!subsidyAccessPolicy?.assignmentConfiguration; - const assignableSubsidyAccessPolicyTypes = ['AssignedLearnerCreditAccessPolicy']; - return isAssignable && assignableSubsidyAccessPolicyTypes.includes(policyType); -}; +import { isAssignableSubsidyAccessPolicyType } from '../../../../utils'; /** * Retrieves a subsidy access policy by UUID from the API. @@ -21,7 +15,7 @@ const getSubsidyAccessPolicy = async ({ queryKey }) => { const subsidyAccessPolicyUUID = queryKey[2]; const response = await EnterpriseAccessApiService.retrieveSubsidyAccessPolicy(subsidyAccessPolicyUUID); const subsidyAccessPolicy = camelCaseObject(response.data); - subsidyAccessPolicy.isAssignable = determineBudgetAssignability(subsidyAccessPolicy); + subsidyAccessPolicy.isAssignable = isAssignableSubsidyAccessPolicyType(subsidyAccessPolicy); return subsidyAccessPolicy; }; diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 50e8972145..b030cb6d17 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -212,6 +212,7 @@ describe('', () => { pending: isAssignableBudget ? mockBudgetAggregates.pending : undefined, spent: mockBudgetAggregates.spent, }, + isAssignable: isAssignableBudget, }; useSubsidySummaryAnalyticsApi.mockReturnValue({ isLoading: false, diff --git a/src/utils.js b/src/utils.js index 207d25c8bc..f3fa4e103e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -412,6 +412,17 @@ function defaultQueryClientRetryHandler(failureCount, err) { return true; } +/** + * Determines whether a subsidy access policy is assignable, based on its policy type + * and the presence of an assignment configuration. + */ +function isAssignableSubsidyAccessPolicyType(policy) { + const policyType = policy?.policyType; + const isAssignable = !!policy?.assignmentConfiguration; + const assignableSubsidyAccessPolicyTypes = ['AssignedLearnerCreditAccessPolicy']; + return isAssignable && assignableSubsidyAccessPolicyTypes.includes(policyType); +} + export { camelCaseDict, camelCaseDictArray, @@ -446,4 +457,5 @@ export { pollAsync, isNotValidNumberString, defaultQueryClientRetryHandler, + isAssignableSubsidyAccessPolicyType, }; From 7b59077a041b30654c7e66e53b0deac705149ea7 Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:36:01 -0700 Subject: [PATCH 099/124] feat: add remind functionality (#1123) * feat: add remind functionality * fix: adding additional test coverage * fix: PR requests * fix: final review --- .../AssignmentRowActionTableCell.jsx | 22 +-- .../AssignmentStatusTableCell.jsx | 5 +- .../AssignmentTableCancel.jsx | 4 +- .../AssignmentTableRemind.jsx | 38 +++-- .../BudgetDetailPageWrapper.jsx | 28 +++- .../CancelAssignmentModal.jsx | 8 +- .../PendingAssignmentCancelButton.jsx | 4 +- .../PendingAssignmentRemindButton.jsx | 50 ++++++ .../RemindAssignmentModal.jsx | 71 ++++++++ .../FailedReminder.jsx | 56 +++++++ .../cards/NewAssignmentModalButton.jsx | 1 + .../data/hooks/index.js | 1 + .../data/hooks/useCancelContentAssignments.js | 10 +- .../useCancelContentAssignments.test.jsx | 12 +- .../data/hooks/useRemindContentAssignments.js | 42 +++++ .../useRemindContentAssignments.test.jsx | 141 ++++++++++++++++ .../useSuccessfulReminderToastContextValue.js | 39 +++++ .../tests/BudgetDetailPage.test.jsx | 151 ++++++++++++++++++ .../tests/BudgetDetailPageWrapper.test.jsx | 59 +++++++ .../services/EnterpriseAccessApiService.js | 11 ++ .../tests/EnterpriseAccessApiService.test.js | 11 ++ 21 files changed, 710 insertions(+), 54 deletions(-) create mode 100644 src/components/learner-credit-management/PendingAssignmentRemindButton.jsx create mode 100644 src/components/learner-credit-management/RemindAssignmentModal.jsx create mode 100644 src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx create mode 100644 src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js create mode 100644 src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx create mode 100644 src/components/learner-credit-management/data/hooks/useSuccessfulReminderToastContextValue.js diff --git a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx index 74398b32c5..fbe36161fe 100644 --- a/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentRowActionTableCell.jsx @@ -1,31 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - Icon, IconButton, OverlayTrigger, Stack, Tooltip, -} from '@edx/paragon'; -import { Mail } from '@edx/paragon/icons'; +import { Stack } from '@edx/paragon'; +import PendingAssignmentRemindButton from './PendingAssignmentRemindButton'; import PendingAssignmentCancelButton from './PendingAssignmentCancelButton'; const AssignmentRowActionTableCell = ({ row }) => { const isLearnerStateWaiting = row.original.learnerState === 'waiting'; - const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; return ( {isLearnerStateWaiting && ( - Remind learner} - > - console.log(`Reminding ${row.original.uuid}`)} - data-testid={`remind-assignment-${row.original.uuid}`} - /> - + )} diff --git a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx index 63dffc765c..10cb79caf9 100644 --- a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx @@ -4,6 +4,7 @@ import { import PropTypes from 'prop-types'; import FailedBadEmail from './assignments-status-chips/FailedBadEmail'; import FailedCancellation from './assignments-status-chips/FailedCancellation'; +import FailedReminder from './assignments-status-chips/FailedReminder'; import FailedSystem from './assignments-status-chips/FailedSystem'; import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; import WaitingForLearner from './assignments-status-chips/WaitingForLearner'; @@ -44,10 +45,12 @@ const AssignmentStatusTableCell = ({ row }) => { } return ; } - if (errorReason.actionType === 'cancelled') { return ; } + if (errorReason.actionType === 'reminded') { + return ; + } } // Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway. diff --git a/src/components/learner-credit-management/AssignmentTableCancel.jsx b/src/components/learner-credit-management/AssignmentTableCancel.jsx index cb45c1f4eb..a160934ef4 100644 --- a/src/components/learner-credit-management/AssignmentTableCancel.jsx +++ b/src/components/learner-credit-management/AssignmentTableCancel.jsx @@ -9,7 +9,7 @@ const AssignmentTableCancelAction = ({ selectedFlatRows }) => { const assignmentUuids = selectedFlatRows.map(row => row.id); const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; const { - assignButtonState, + cancelButtonState, cancelContentAssignments, close, isOpen, @@ -25,7 +25,7 @@ const AssignmentTableCancelAction = ({ selectedFlatRows }) => { cancelContentAssignments={cancelContentAssignments} close={close} isOpen={isOpen} - assignButtonState={assignButtonState} + cancelButtonState={cancelButtonState} uuidCount={assignmentUuids.length} /> diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx index cc62c9a703..e63fbfac72 100644 --- a/src/components/learner-credit-management/AssignmentTableRemind.jsx +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -2,18 +2,38 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from '@edx/paragon'; import { Mail } from '@edx/paragon/icons'; +import useRemindContentAssignments from './data/hooks/useRemindContentAssignments'; +import RemindAssignmentModal from './RemindAssignmentModal'; -const AssignmentTableRemindAction = ({ selectedFlatRows, ...rest }) => { +const AssignmentTableRemindAction = ({ selectedFlatRows }) => { + const assignmentUuids = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').map(({ id }) => id); + const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; const selectedRemindableRows = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').length; + const { + remindButtonState, + remindContentAssignments, + close, + isOpen, + open, + } = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids); return ( - + <> + + + ); }; diff --git a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx index 0702441137..41904c2938 100644 --- a/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageWrapper.jsx @@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet'; import { Container, Toast } from '@edx/paragon'; import Hero from '../Hero'; -import { useSuccessfulAssignmentToastContextValue, useSuccessfulCancellationToastContextValue } from './data'; +import { useSuccessfulAssignmentToastContextValue, useSuccessfulCancellationToastContextValue, useSuccessfulReminderToastContextValue } from './data'; const PAGE_TITLE = 'Learner Credit Management'; @@ -22,6 +22,7 @@ const BudgetDetailPageWrapper = ({ const successfulAssignmentToast = useSuccessfulAssignmentToastContextValue(); const successfulCancellationToast = useSuccessfulCancellationToastContextValue(); + const successfulReminderToast = useSuccessfulReminderToastContextValue(); const { isSuccessfulAssignmentAllocationToastOpen, @@ -35,10 +36,17 @@ const BudgetDetailPageWrapper = ({ closeToastForAssignmentCancellation, } = successfulCancellationToast; + const { + isSuccessfulAssignmentReminderToastOpen, + successfulAssignmentReminderToastMessage, + closeToastForAssignmentReminder, + } = successfulReminderToast; + const values = useMemo(() => ({ successfulAssignmentToast, successfulCancellationToast, - }), [successfulAssignmentToast, successfulCancellationToast]); + successfulReminderToast, + }), [successfulAssignmentToast, successfulCancellationToast, successfulReminderToast]); return ( {/** - Successful assignment allocation and cancellation Toast notifications. It is rendered here to guarantee that the - Toast component will not be unmounted when the user programmatically navigates to the "Activity" - tab, which will unmount the course cards that rendered the assignment modal. Thus, the Toast must - be rendered within the component tree that's common to both the "Activity" and "Overview" tabs. + Successful assignment allocation, reminder, and cancellation Toast notifications. It is rendered + here to guarantee that the Toast component will not be unmounted when the user programmatically + navigates to the "Activity" tab, which will unmount the course cards that rendered the assignment + modal. Thus, the Toast must be rendered within the component tree that's common to both the + "Activity" and "Overview" tabs. */} {successfulAssignmentCancellationToastMessage} + + + {successfulAssignmentReminderToastMessage} + ); }; diff --git a/src/components/learner-credit-management/CancelAssignmentModal.jsx b/src/components/learner-credit-management/CancelAssignmentModal.jsx index 2636fdb13d..cce7b896b1 100644 --- a/src/components/learner-credit-management/CancelAssignmentModal.jsx +++ b/src/components/learner-credit-management/CancelAssignmentModal.jsx @@ -7,7 +7,7 @@ import { DoNotDisturbOn } from '@edx/paragon/icons'; import { BudgetDetailPageContext } from './BudgetDetailPageWrapper'; const CancelAssignmentModal = ({ - assignButtonState, + cancelButtonState, cancelContentAssignments, close, isOpen, @@ -46,7 +46,7 @@ const CancelAssignmentModal = ({ Go back 1 ? `Cancel assignments (${uuidCount})` : 'Cancel assignment', pending: 'Canceling...', @@ -54,7 +54,7 @@ const CancelAssignmentModal = ({ error: 'Try again', }} variant="danger" - state={assignButtonState} + state={cancelButtonState} onClick={handleOnClick} /> @@ -64,7 +64,7 @@ const CancelAssignmentModal = ({ }; CancelAssignmentModal.propTypes = { - assignButtonState: PropTypes.string.isRequired, + cancelButtonState: PropTypes.string.isRequired, cancelContentAssignments: PropTypes.func.isRequired, close: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, diff --git a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx index 692cdd4cfb..5b2144b149 100644 --- a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx +++ b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx @@ -10,7 +10,7 @@ import CancelAssignmentModal from './CancelAssignmentModal'; const PendingAssignmentCancelButton = ({ row }) => { const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; const { - assignButtonState, + cancelButtonState, cancelContentAssignments, close, isOpen, @@ -29,7 +29,7 @@ const PendingAssignmentCancelButton = ({ row }) => { variant="danger" /> { + const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; + const { + remindButtonState, + remindContentAssignments, + close, + isOpen, + open, + } = useRemindContentAssignments(row.original.assignmentConfiguration, [row.original.uuid]); + + return ( + <> + + + + ); +}; + +PendingAssignmentRemindButton.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + assignmentConfiguration: PropTypes.string.isRequired, + learnerEmail: PropTypes.string, + uuid: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; + +export default PendingAssignmentRemindButton; diff --git a/src/components/learner-credit-management/RemindAssignmentModal.jsx b/src/components/learner-credit-management/RemindAssignmentModal.jsx new file mode 100644 index 0000000000..013535e2b9 --- /dev/null +++ b/src/components/learner-credit-management/RemindAssignmentModal.jsx @@ -0,0 +1,71 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { + ActionRow, ModalDialog, StatefulButton, +} from '@edx/paragon'; +import { Mail } from '@edx/paragon/icons'; +import { BudgetDetailPageContext } from './BudgetDetailPageWrapper'; + +const RemindAssignmentModal = ({ + remindButtonState, remindContentAssignments, close, isOpen, uuidCount, +}) => { + const { + successfulReminderToast: { displayToastForAssignmentReminder }, + } = useContext(BudgetDetailPageContext); + + const handleOnClick = async () => { + await remindContentAssignments(); + displayToastForAssignmentReminder(uuidCount); + }; + + return ( + + + + Remind learner? + + + +

    You are sending a reminder email to the selected learner + to take the next step on the course you assigned. +

    +

    When your learner completes enrollment, the associated + "assigned" funds will be marked as "spent". +

    +
    + + + + Go back + 1 ? `Send reminders (${uuidCount})` : 'Send reminder', + pending: 'Reminding...', + complete: 'Reminded', + error: 'Try again', + }} + variant="danger" + state={remindButtonState} + onClick={handleOnClick} + /> + + +
    + ); +}; + +RemindAssignmentModal.propTypes = { + remindButtonState: PropTypes.string.isRequired, + remindContentAssignments: PropTypes.func.isRequired, + close: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + uuidCount: PropTypes.number, +}; + +export default RemindAssignmentModal; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx new file mode 100644 index 0000000000..d3b9533bb9 --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import { Chip, useToggle, Hyperlink } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; +import BaseModalPopup from './BaseModalPopup'; + +const FailedReminder = () => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = useState(null); + + return ( + <> + + Failed: Reminder + + + + Failed: Reminder + + +

    Your reminder email did not send. Something went wrong behind the scenes.

    +
    +

    Suggested resolution steps

    +
      +
    • + Wait and try to send this reminder again later, or send an email directly outside of the system. +
    • +
    • + If the issue continues, contact customer support. +
    • +
    • + Get more troubleshooting help at{' '} + + Help Center: Course Assignments + . +
    • +
    +
    +
    +
    + + ); +}; + +export default FailedReminder; diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index e01816489e..47a3899735 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -197,6 +197,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_HELP_CENTER, )} destination="https://edx.org" + showLaunchIcon target="_blank" > Help Center: Course Assignments diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index 400f754b57..d979f3867a 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -9,3 +9,4 @@ export { default as useBudgetDetailActivityOverview } from './useBudgetDetailAct export { default as useIsLargeOrGreater } from './useIsLargeOrGreater'; export { default as useSuccessfulAssignmentToastContextValue } from './useSuccessfulAssignmentToastContextValue'; export { default as useSuccessfulCancellationToastContextValue } from './useSuccessfulCancellationToastContextValue'; +export { default as useSuccessfulReminderToastContextValue } from './useSuccessfulReminderToastContextValue'; diff --git a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js index 8b9bec8d6f..189462a776 100644 --- a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js +++ b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js @@ -12,26 +12,26 @@ const useCancelContentAssignments = ( assignmentUuids, ) => { const [isOpen, open, close] = useToggle(false); - const [assignButtonState, setAssignButtonState] = useState('default'); + const [cancelButtonState, setCancelButtonState] = useState('default'); const queryClient = useQueryClient(); const { subsidyAccessPolicyId } = useBudgetId(); const cancelContentAssignments = useCallback(async () => { - setAssignButtonState('pending'); + setCancelButtonState('pending'); try { await EnterpriseAccessApiService.cancelContentAssignments(assignmentConfigurationUuid, assignmentUuids); - setAssignButtonState('complete'); + setCancelButtonState('complete'); queryClient.invalidateQueries({ queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), }); } catch (err) { logError(err); - setAssignButtonState('error'); + setCancelButtonState('error'); } }, [assignmentConfigurationUuid, assignmentUuids, queryClient, subsidyAccessPolicyId]); return { - assignButtonState, + cancelButtonState, cancelContentAssignments, close, isOpen, diff --git a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx index 4044003fef..e8bcad9513 100644 --- a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx @@ -49,7 +49,7 @@ describe('useCancelContentAssignments', () => { ); expect(result.current).toEqual({ - assignButtonState: 'default', + cancelButtonState: 'default', cancelContentAssignments: expect.any(Function), close: expect.any(Function), isOpen: false, @@ -63,7 +63,7 @@ describe('useCancelContentAssignments', () => { expect(logError).toBeCalledTimes(0); expect(result.current).toEqual({ - assignButtonState: 'complete', + cancelButtonState: 'complete', cancelContentAssignments: expect.any(Function), close: expect.any(Function), isOpen: false, @@ -82,7 +82,7 @@ describe('useCancelContentAssignments', () => { ); expect(result.current).toEqual({ - assignButtonState: 'default', + cancelButtonState: 'default', cancelContentAssignments: expect.any(Function), close: expect.any(Function), isOpen: false, @@ -96,7 +96,7 @@ describe('useCancelContentAssignments', () => { expect(logError).toBeCalledTimes(0); expect(result.current).toEqual({ - assignButtonState: 'complete', + cancelButtonState: 'complete', cancelContentAssignments: expect.any(Function), close: expect.any(Function), isOpen: false, @@ -116,7 +116,7 @@ describe('useCancelContentAssignments', () => { ); expect(result.current).toEqual({ - assignButtonState: 'default', + cancelButtonState: 'default', cancelContentAssignments: expect.any(Function), close: expect.any(Function), isOpen: false, @@ -131,7 +131,7 @@ describe('useCancelContentAssignments', () => { expect(logError).toBeCalledTimes(1); expect(result.current).toEqual({ - assignButtonState: 'error', + cancelButtonState: 'error', cancelContentAssignments: expect.any(Function), close: expect.any(Function), isOpen: false, diff --git a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js new file mode 100644 index 0000000000..bc55743a30 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js @@ -0,0 +1,42 @@ +import { useCallback, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { logError } from '@edx/frontend-platform/logging'; +import { useToggle } from '@edx/paragon'; + +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import { learnerCreditManagementQueryKeys } from '../constants'; +import useBudgetId from './useBudgetId'; + +const useRemindContentAssignments = ( + assignmentConfigurationUuid, + assignmentUuids, +) => { + const [isOpen, open, close] = useToggle(false); + const [remindButtonState, setRemindButtonState] = useState('default'); + const queryClient = useQueryClient(); + const { subsidyAccessPolicyId } = useBudgetId(); + + const remindContentAssignments = useCallback(async () => { + setRemindButtonState('pending'); + try { + await EnterpriseAccessApiService.remindContentAssignments(assignmentConfigurationUuid, assignmentUuids); + setRemindButtonState('complete'); + queryClient.invalidateQueries({ + queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), + }); + } catch (err) { + logError(err); + setRemindButtonState('error'); + } + }, [assignmentConfigurationUuid, assignmentUuids, queryClient, subsidyAccessPolicyId]); + + return { + remindButtonState, + remindContentAssignments, + close, + isOpen, + open, + }; +}; + +export default useRemindContentAssignments; diff --git a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx new file mode 100644 index 0000000000..88133f99f1 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx @@ -0,0 +1,141 @@ +import { useParams } from 'react-router-dom'; +import { renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor } from '@testing-library/react'; +import { QueryClientProvider } from '@tanstack/react-query'; + +import { logError } from '@edx/frontend-platform/logging'; + +import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; +import useRemindContentAssignments from './useRemindContentAssignments'; +import { queryClient } from '../../../test/testUtils'; + +const TEST_ASSIGNMENT_CONFIGURATION_UUID = 'test-assignment-configuration-uuid'; +const TEST_PENDING_ASSIGNMENT_UUID_1 = 'test-pending-assignment-uuid_1'; +const TEST_PENDING_ASSIGNMENT_UUID_2 = 'test-pending-assignment-uuid_2'; + +const wrapper = ({ children }) => ( + {children} +); + +jest.mock('../../../../data/services/EnterpriseAccessApiService'); +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})); + +describe('useRemindContentAssignments', () => { + beforeEach(() => { + useParams.mockReturnValue({ + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should send a post request to remind a single pending assignment', async () => { + EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 }); + const { result } = renderHook( + () => useRemindContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + TEST_PENDING_ASSIGNMENT_UUID_1, + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + remindButtonState: 'default', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => result.current.remindContentAssignments()); + expect( + EnterpriseAccessApiService.remindContentAssignments, + ).toHaveBeenCalled(); + expect(logError).toBeCalledTimes(0); + + expect(result.current).toEqual({ + remindButtonState: 'complete', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); + + it('should send a post request to remind multiple pending assignments', async () => { + EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 }); + const { result } = renderHook( + () => useRemindContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + [TEST_PENDING_ASSIGNMENT_UUID_1, TEST_PENDING_ASSIGNMENT_UUID_2], + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + remindButtonState: 'default', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => result.current.remindContentAssignments()); + expect( + EnterpriseAccessApiService.remindContentAssignments, + ).toHaveBeenCalled(); + expect(logError).toBeCalledTimes(0); + + expect(result.current).toEqual({ + remindButtonState: 'complete', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); + + it('should handle assignment reminder error', async () => { + const error = new Error('An error occurred'); + EnterpriseAccessApiService.remindContentAssignments.mockRejectedValueOnce(error); + const { result } = renderHook( + () => useRemindContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + [TEST_PENDING_ASSIGNMENT_UUID_1, TEST_PENDING_ASSIGNMENT_UUID_2], + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + remindButtonState: 'default', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => result.current.remindContentAssignments()); + + expect( + EnterpriseAccessApiService.remindContentAssignments, + ).toHaveBeenCalled(); + expect(logError).toBeCalledTimes(1); + + expect(result.current).toEqual({ + remindButtonState: 'error', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); +}); diff --git a/src/components/learner-credit-management/data/hooks/useSuccessfulReminderToastContextValue.js b/src/components/learner-credit-management/data/hooks/useSuccessfulReminderToastContextValue.js new file mode 100644 index 0000000000..27faaa9918 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useSuccessfulReminderToastContextValue.js @@ -0,0 +1,39 @@ +import { useCallback, useMemo, useState } from 'react'; + +const generateSuccessRemindMessage = (assignmentUuidCount) => { + if (assignmentUuidCount > 1) { + return `Reminders sent (${assignmentUuidCount})`; + } + return 'Reminder sent'; +}; + +const useSuccessfulReminderToastContextValue = () => { + const [isToastOpen, setIsToastOpen] = useState(false); + const [assignmentUuidCount, setAssignmentUuidCount] = useState(); + + const handleDisplayToast = useCallback((assignmentUuids) => { + setIsToastOpen(true); + setAssignmentUuidCount(assignmentUuids); + }, []); + + const handleCloseToast = useCallback(() => { + setIsToastOpen(false); + }, []); + + const successfulAssignmentReminderToastMessage = generateSuccessRemindMessage(assignmentUuidCount); + + const successfulReminderToastContextValue = useMemo(() => ({ + isSuccessfulAssignmentReminderToastOpen: isToastOpen, + displayToastForAssignmentReminder: handleDisplayToast, + closeToastForAssignmentReminder: handleCloseToast, + successfulAssignmentReminderToastMessage, + }), [ + isToastOpen, + handleDisplayToast, + handleCloseToast, + successfulAssignmentReminderToastMessage, + ]); + return successfulReminderToastContextValue; +}; + +export default useSuccessfulReminderToastContextValue; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index ac73a03d64..aee974de7c 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -150,6 +150,13 @@ const mockFailedCancelledLearnerAction = { completedAt: null, errorReason: 'email_error', }; + +const mockFailedReminderLearnerAction = { + actionType: 'reminded', + completedAt: null, + errorReason: 'email_error', +}; + const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; @@ -863,6 +870,18 @@ describe('', () => { actionType: 'cancelled', }, }, + { + learnerState: 'failed', + hasLearnerEmail: true, + expectedChipStatus: 'Failed: Reminder', + expectedModalPopupHeading: 'Failed: Reminder', + expectedModalPopupContent: 'Something went wrong behind the scenes.', + actions: [mockFailedReminderLearnerAction], + errorReason: { + errorReason: 'internal_api_error', + actionType: 'reminded', + }, + }, ])('renders correct status chips with assigned table data (%s)', ({ learnerState, hasLearnerEmail, @@ -1277,6 +1296,82 @@ describe('', () => { ); }); + it('reminds assignments in bulk', async () => { + EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 }); + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useBudgetRedemptions.mockReturnValue({ + isLoading: false, + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 2, + results: [ + { + uuid: 'test-uuid1', + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + }, + { + uuid: 'test-uuid2', + contentKey: mockCourseKey, + contentQuantity: -29900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-11-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + }, + ], + learnerStateCounts: [ + { learnerState: 'waiting', count: 1 }, + { learnerState: 'waiting', count: 1 }, + ], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + renderWithRouter(); + const remindRowAction = screen.getByTitle('Toggle All Current Page Rows Selected'); + expect(remindRowAction).toBeInTheDocument(); + userEvent.click(remindRowAction); + const remindBulkActionButton = screen.getByText('Remind (2)'); + expect(remindBulkActionButton).toBeInTheDocument(); + userEvent.click(remindBulkActionButton); + const modalDialog = screen.getByRole('dialog'); + expect(modalDialog).toBeInTheDocument(); + const remindDialogButton = getButtonElement('Send reminders (2)'); + userEvent.click(remindDialogButton); + expect( + EnterpriseAccessApiService.remindContentAssignments, + ).toHaveBeenCalled(); + await waitFor( + () => expect(screen.getByText('Reminders sent (2)')).toBeInTheDocument(), + ); + }); + it('cancels a single assignment', async () => { EnterpriseAccessApiService.cancelContentAssignments.mockResolvedValueOnce({ status: 200 }); useParams.mockReturnValue({ @@ -1333,4 +1428,60 @@ describe('', () => { () => expect(screen.getByText('Assignment canceled')).toBeInTheDocument(), ); }); + it('reminds a single assignment', async () => { + EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 }); + useParams.mockReturnValue({ + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useBudgetRedemptions.mockReturnValue({ + isLoading: false, + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 1, + results: [ + { + uuid: 'test-uuid', + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + }, + ], + learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + renderWithRouter(); + const remindIconButton = screen.getByTestId('remind-assignment-test-uuid'); + expect(remindIconButton).toBeInTheDocument(); + userEvent.click(remindIconButton); + const modalDialog = screen.getByRole('dialog'); + expect(modalDialog).toBeInTheDocument(); + const remindDialogButton = getButtonElement('Send reminder'); + userEvent.click(remindDialogButton); + await waitFor( + () => expect(screen.getByText('Reminder sent')).toBeInTheDocument(), + ); + }); }); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx index a5d07d93ae..96c7ed5fc2 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx @@ -204,4 +204,63 @@ describe('', () => { expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); }); }); + + it.each([ + { + assignmentUUIDs: 1, + }, + { + assignmentUUIDs: 2, + }, + ])('should render Toast notification for successful assignment reminder (%s)', async ({ + assignmentUUIDs, + }) => { + const ToastContextController = () => { + const { + successfulReminderToast: { + displayToastForAssignmentReminder, + closeToastForAssignmentReminder, + }, + } = useContext(BudgetDetailPageContext); + + const handleDisplayToast = () => { + displayToastForAssignmentReminder(assignmentUUIDs); + }; + + const handleCloseToast = () => { + closeToastForAssignmentReminder(); + }; + + return ( +
    + + +
    + ); + }; + render(); + + const toastMessages = []; + if (assignmentUUIDs > 1) { + toastMessages.push(`Reminders sent (${assignmentUUIDs})`); + } + if (assignmentUUIDs === 1) { + toastMessages.push('Reminder sent'); + } + const expectedToastMessage = toastMessages.join(' '); + + // Open Toast notification + userEvent.click(getButtonElement('Open Toast')); + + // Verify Toast notification is rendered + expect(screen.getByText(expectedToastMessage)).toBeInTheDocument(); + + // Close Toast notification + userEvent.click(getButtonElement('Close Toast')); + + // Verify Toast notification is no longer rendered + await waitFor(() => { + expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index cb7e0da9a5..8a38cb2b14 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -184,6 +184,17 @@ class EnterpriseAccessApiService { return EnterpriseAccessApiService.apiClient().post(url, options); } + /** + * Remind content assignments for a specific AssignmentConfiguration. + */ + static remindContentAssignments(assignmentConfigurationUUID, assignmentUuids) { + const options = { + assignment_uuids: assignmentUuids, + }; + const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/remind/`; + return EnterpriseAccessApiService.apiClient().post(url, options); + } + /** * Retrieve a specific subsidy access policy. * @param {string} subsidyAccessPolicyUUID The UUID of the subsidy access policy to retrieve. diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index 51fe177d49..f2007a56f3 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -200,4 +200,15 @@ describe('EnterpriseAccessApiService', () => { options, ); }); + + test('remindContentAssignments calls enterprise-access cancel POST API to remind learners', () => { + const options = { + assignment_uuids: mockAssignmentUUIDs, + }; + EnterpriseAccessApiService.remindContentAssignments(mockAssignmentConfigurationUUID, mockAssignmentUUIDs); + expect(axios.post).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/remind/`, + options, + ); + }); }); From 492c0bcb2d2005e644de4197c633ea1b2b881df4 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 7 Dec 2023 08:22:41 -0500 Subject: [PATCH 100/124] fix: disables refetch on window focus for useEnterpriseBudgets useQuery hook call (#1125) --- src/components/EnterpriseSubsidiesContext/data/hooks.js | 3 +++ src/components/ProductTours/data/hooks.js | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index d30b9d2e3f..58655e1e39 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -117,6 +117,9 @@ export const useEnterpriseBudgets = ({ enterpriseId, enablePortalLearnerCreditManagementScreen, }), + // Disables the ability to re-fetch on window focus due to re-fetching behavior causing + // ProductTours component to rerender despite localStorage flagging + refetchOnWindowFocus: false, }); export const useCustomerAgreement = ({ enterpriseId }) => { diff --git a/src/components/ProductTours/data/hooks.js b/src/components/ProductTours/data/hooks.js index daff89f8b1..858ecfaf64 100644 --- a/src/components/ProductTours/data/hooks.js +++ b/src/components/ProductTours/data/hooks.js @@ -9,9 +9,9 @@ import { SubsidyRequestsContext } from '../../subsidy-requests'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; export const usePortalAppearanceTour = ({ enablePortalAppearance }) => { - const dismissedLearnerCreditTourCookie = global.localStorage.getItem(PORTAL_APPEARANCE_TOUR_COOKIE_NAME); + const dismissedPortalAppearanceTour = global.localStorage.getItem(PORTAL_APPEARANCE_TOUR_COOKIE_NAME); // Only show tour if feature is enabled, hide cookie is undefined or false or not in the settings page - const showPortalAppearanceTour = enablePortalAppearance && !dismissedLearnerCreditTourCookie; + const showPortalAppearanceTour = enablePortalAppearance && !dismissedPortalAppearanceTour; const [portalAppearanceTourEnabled, setPortalAppearanceTourEnabled] = useState(showPortalAppearanceTour); return [portalAppearanceTourEnabled, setPortalAppearanceTourEnabled]; }; From d727e731cfc6f0651e0e6fa13632eeb6b65eb92b Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 7 Dec 2023 09:02:09 -0500 Subject: [PATCH 101/124] Revert "fix: disables refetch on window focus for useEnterpriseBudgets useQuery hook call (#1125)" (#1127) This reverts commit 492c0bcb2d2005e644de4197c633ea1b2b881df4. --- src/components/EnterpriseSubsidiesContext/data/hooks.js | 3 --- src/components/ProductTours/data/hooks.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index 58655e1e39..d30b9d2e3f 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -117,9 +117,6 @@ export const useEnterpriseBudgets = ({ enterpriseId, enablePortalLearnerCreditManagementScreen, }), - // Disables the ability to re-fetch on window focus due to re-fetching behavior causing - // ProductTours component to rerender despite localStorage flagging - refetchOnWindowFocus: false, }); export const useCustomerAgreement = ({ enterpriseId }) => { diff --git a/src/components/ProductTours/data/hooks.js b/src/components/ProductTours/data/hooks.js index 858ecfaf64..daff89f8b1 100644 --- a/src/components/ProductTours/data/hooks.js +++ b/src/components/ProductTours/data/hooks.js @@ -9,9 +9,9 @@ import { SubsidyRequestsContext } from '../../subsidy-requests'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; export const usePortalAppearanceTour = ({ enablePortalAppearance }) => { - const dismissedPortalAppearanceTour = global.localStorage.getItem(PORTAL_APPEARANCE_TOUR_COOKIE_NAME); + const dismissedLearnerCreditTourCookie = global.localStorage.getItem(PORTAL_APPEARANCE_TOUR_COOKIE_NAME); // Only show tour if feature is enabled, hide cookie is undefined or false or not in the settings page - const showPortalAppearanceTour = enablePortalAppearance && !dismissedPortalAppearanceTour; + const showPortalAppearanceTour = enablePortalAppearance && !dismissedLearnerCreditTourCookie; const [portalAppearanceTourEnabled, setPortalAppearanceTourEnabled] = useState(showPortalAppearanceTour); return [portalAppearanceTourEnabled, setPortalAppearanceTourEnabled]; }; From f9bfbefb6e36f09f8aec2df5c757c7dd9ecb98fc Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Tue, 5 Dec 2023 22:54:06 +0000 Subject: [PATCH 102/124] feat: adding sso orchestrator failure and timeout handling and general formatting cleanup --- src/components/forms/ValidatedFormRadio.tsx | 6 +- .../SettingsSSOTab/NewExistingSSOConfigs.jsx | 35 ++++++++ .../SettingsSSOTab/NewSSOConfigAlerts.jsx | 84 +++++++++++++++++-- .../SettingsSSOTab/NewSSOConfigCard.jsx | 18 ++-- .../settings/SettingsSSOTab/index.jsx | 6 +- .../steps/NewSSOConfigConfigureStep.tsx | 2 +- .../steps/NewSSOConfigConfirmStep.tsx | 2 +- .../steps/NewSSOConfigConnectStep.tsx | 30 +++---- .../tests/NewExistingSSOConfigs.test.jsx | 68 +++++++++++++++ .../tests/NewSSOConfigAlerts.test.jsx | 18 ++++ .../settings/SettingsSSOTab/utils.js | 6 +- src/components/settings/settings.scss | 4 + 12 files changed, 243 insertions(+), 36 deletions(-) diff --git a/src/components/forms/ValidatedFormRadio.tsx b/src/components/forms/ValidatedFormRadio.tsx index bfc29fb2d2..4f4a6f406f 100644 --- a/src/components/forms/ValidatedFormRadio.tsx +++ b/src/components/forms/ValidatedFormRadio.tsx @@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'; import omit from 'lodash/omit'; import isString from 'lodash/isString'; -import { Form } from '@edx/paragon'; +import { Form, Stack } from '@edx/paragon'; import { setFormFieldAction } from './data/actions'; import { useFormContext } from './FormContext'; @@ -65,7 +65,9 @@ const ValidatedFormRadio = (props: ValidatedFormRadioProps) => { isInline={formRadioProps.isInline} value={value} > - {createOptions(formRadioProps.options)} + + {createOptions(formRadioProps.options)} + {formRadioProps.fieldInstructions && ( {formRadioProps.fieldInstructions} diff --git a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx index bdfc1b910f..792e6913f7 100644 --- a/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx +++ b/src/components/settings/SettingsSSOTab/NewExistingSSOConfigs.jsx @@ -26,6 +26,8 @@ const NewExistingSSOConfigs = ({ const [inProgressConfigs, setInProgressConfigs] = useState([]); const [untestedConfigs, setUntestedConfigs] = useState([]); const [liveConfigs, setLiveConfigs] = useState([]); + const [erroredConfigs, setErroredConfigs] = useState([]); + const [timedOutConfigs, setTimedOutConfigs] = useState([]); const [notConfiguredConfigs, setNotConfiguredConfigs] = useState([]); const [queryForTestedConfigs, setQueryForTestedConfigs] = useState(false); const [queryForConfiguredConfigs, setQueryForConfiguredConfigs] = useState(false); @@ -88,6 +90,18 @@ const NewExistingSSOConfigs = ({ return null; }; + function checkConfiguring(config) { + return !config.configured_at || config.submitted_at > config.configured_at; + } + + function checkErrored(config) { + return config.errored_at && (config.submitted_at < config.errored_at); + } + + function checkTimedOut(config) { + return config.submitted_at && checkConfiguring(config) && !config.is_pending_configuration && !checkErrored(config); + } + useEffect(() => { const [active, inactive] = _.partition(configs, config => config.active); const inProgress = configs.filter(isInProgressConfig); @@ -97,6 +111,15 @@ const NewExistingSSOConfigs = ({ ); const notConfigured = configs.filter(config => !config.configured_at); + const handleCheckTimedOut = (config) => ( + config.submitted_at && checkConfiguring(config) && !config.is_pending_configuration && !checkErrored(config) + ); + + const timedOut = configs.filter(handleCheckTimedOut); + const errored = configs.filter(checkErrored); + setTimedOutConfigs(timedOut); + setErroredConfigs(errored); + if (live.length >= 1) { setLiveConfigs(live); openAlerts(); @@ -137,6 +160,15 @@ const NewExistingSSOConfigs = ({ config => (config.submitted_at && !config.configured_at) || (config.configured_at < config.submitted_at), ); const untested = res.data.filter(config => !config.validated_at || config.validated_at < config.configured_at); + const timedOut = res.data.filter(checkTimedOut); + const errored = res.data.filter(checkErrored); + if (timedOut.length >= 1) { + setTimedOutConfigs(timedOut); + } + + if (errored.length >= 1) { + setErroredConfigs(errored); + } if (queryForConfiguredConfigs) { if (inProgress.length === 0) { @@ -174,6 +206,9 @@ const NewExistingSSOConfigs = ({ untestedConfigs={untestedConfigs} notConfigured={notConfiguredConfigs} closeAlerts={closeAlerts} + timedOutConfigs={timedOutConfigs} + erroredConfigs={erroredConfigs} + setIsStepperOpen={setIsStepperOpen} /> )} {renderCards('Active', activeConfigs)} diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx index 746c228bbb..0a625449dd 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigAlerts.jsx @@ -1,11 +1,12 @@ -import React from 'react'; +import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { - CheckCircle, Warning, + CheckCircle, Info, Warning, } from '@edx/paragon/icons'; -import { Alert } from '@edx/paragon'; +import { Alert, Button } from '@edx/paragon'; import Cookies from 'universal-cookie'; +import { SSOConfigContext } from './SSOConfigContext'; export const SSO_SETUP_COMPLETION_COOKIE_NAME = 'dismissed-sso-completion-alert'; const SSO_ALERT_OVERRIDE_PARAM = 'sso_alert_override'; @@ -19,7 +20,19 @@ const NewSSOConfigAlerts = ({ contactEmail, closeAlerts, enterpriseSlug, + timedOutConfigs, + erroredConfigs, + setIsStepperOpen, }) => { + const { setProviderConfig } = useContext(SSOConfigContext); + + const configureOnClick = (config) => { + setProviderConfig(config); + setIsStepperOpen(true); + }; + const onTimedOutConfigureClick = () => configureOnClick(timedOutConfigs[0]); + const onErroredConfigClick = () => configureOnClick(erroredConfigs[0]); + const dismissSetupCompleteAlert = () => { ssoCookies.set( SSO_SETUP_COMPLETION_COOKIE_NAME, @@ -32,13 +45,63 @@ const NewSSOConfigAlerts = ({ const searchParams = new URLSearchParams(window.location.search); const dismissedSSOSetupCompletionCookie = ssoCookies.get(SSO_SETUP_COMPLETION_COOKIE_NAME) === 'true'; const hideSSOLiveAlert = dismissedSSOSetupCompletionCookie && !searchParams.get(SSO_ALERT_OVERRIDE_PARAM); + + const [showTimeoutAlert, setShowTimeoutAlert] = useState(true); + const [showErrorAlert, setShowErrorAlert] = useState(true); + + if (timedOutConfigs.length >= 1) { + return ( + Configure, + ]} + dismissible + show={showTimeoutAlert} + onClose={() => { setShowTimeoutAlert(false); }} + stacked + > + SSO Configuration timed out +

    + Your SSO configuration failed due to an internal error. Please try again by selecting “Configure” below and + {' '}verifying your integration details. Then reconfigure, reauthorize, and test your connection. +

    +
    + ); + } + + if (erroredConfigs.length >= 1) { + return ( + Configure, + ]} + dismissible + onClose={() => { setShowErrorAlert(false); }} + stacked + > + SSO Configuration failed +

    + Please verify integration details have been entered correctly. Select “Configure” below and verify your + {' '}integration details. Then reconfigure, reauthorize, and test your connection. +

    +
    + ); + } + return ( <> {inProgressConfigs.length >= 1 && ( @@ -53,21 +116,21 @@ const NewSSOConfigAlerts = ({ You need to test your SSO connection

    - Your SSO configuration has completed, + Your SSO configuration has been completed, and you should have received an email with the following instructions:

    - 1. Copy the URL for your learner Portal dashboard below:
    + 1: Copy the URL for your Learner Portal dashboard below:

      http://courses.edx.org/dashboard?tpa_hint={enterpriseSlug}

    2: Launch a new incognito or private window and paste the copied URL into the URL bar to load your - learner Portal dashboard.
    + Learner Portal dashboard.

    3: When prompted, enter login credentials supported by your IDP to test your connection to edX.

    @@ -82,7 +145,7 @@ const NewSSOConfigAlerts = ({ !hideSSOLiveAlert) && ( ({ diff --git a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx index bcdacdaa2c..47a5141c1d 100644 --- a/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx +++ b/src/components/settings/SettingsSSOTab/NewSSOConfigCard.jsx @@ -19,8 +19,14 @@ const NewSSOConfigCard = ({ }) => { const VALIDATED = config.validated_at; const ENABLED = config.active; - const CONFIGURED = config.configured_at && (config.submitted_at < config.configured_at); - const SUBMITTED = config.submitted_at; + const CONFIGURED = config.configured_at && (config.submitted_at < config.configured_at) && ( + !config.errored_at || (config.errored_at && config.configured_at > config.errored_at) + ); + const SUBMITTED = config.submitted_at && ( + !config.errored_at || (config.errored_at && config.submitted_at > config.errored_at) + ); + const ERRORED = config.errored_at; + const TIMED_OUT = SUBMITTED && !CONFIGURED && !config.is_pending_configuration; const { setProviderConfig } = useContext(SSOConfigContext); @@ -130,7 +136,7 @@ const NewSSOConfigCard = ({ const renderCardButton = () => ( <> - {!VALIDATED && CONFIGURED && ( + {((!VALIDATED && CONFIGURED) || ((TIMED_OUT) || (ERRORED))) && (

    )} - actions={(!SUBMITTED || CONFIGURED) && ( + actions={((!SUBMITTED || CONFIGURED) || (ERRORED || TIMED_OUT)) && ( )} - {(!ENABLED || !VALIDATED) && ( + {((!ENABLED || !VALIDATED) || (ERRORED || TIMED_OUT)) && ( onDeleteClick(config)} @@ -225,6 +231,8 @@ NewSSOConfigCard.propTypes = { validated_at: PropTypes.string, configured_at: PropTypes.string, submitted_at: PropTypes.string, + errored_at: PropTypes.string, + is_pending_configuration: PropTypes.bool, }).isRequired, setLoading: PropTypes.func.isRequired, setRefreshBool: PropTypes.func.isRequired, diff --git a/src/components/settings/SettingsSSOTab/index.jsx b/src/components/settings/SettingsSSOTab/index.jsx index e98c94840b..e9b0bf5825 100644 --- a/src/components/settings/SettingsSSOTab/index.jsx +++ b/src/components/settings/SettingsSSOTab/index.jsx @@ -14,7 +14,7 @@ import NewSSOConfigForm from './NewSSOConfigForm'; import { SSOConfigContext, SSOConfigContextProvider } from './SSOConfigContext'; import LmsApiService from '../../../data/services/LmsApiService'; import { features } from '../../../config'; -import { isInProgressConfig } from './utils'; +import { isInProgressConfig, checkErroredOrTimedOutConfig } from './utils'; const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { const { @@ -59,7 +59,9 @@ const SettingsSSOTab = ({ enterpriseId, setHasSSOConfig }) => { if (AUTH0_SELF_SERVICE_INTEGRATION) { const newButtonVisible = existingConfigs?.length > 0 && (providerConfig === null); - const newButtonDisabled = existingConfigs.some(isInProgressConfig); + const newButtonDisabled = existingConfigs.some(isInProgressConfig) && ( + !existingConfigs.some(checkErroredOrTimedOutConfig) + ); return (
    { formId="displayName" type="text" floatingLabel="Display Name (Optional)" - fieldInstructions="Create a custom display name for this SSO integration" + fieldInstructions="Create a custom display name for this SSO integration." /> diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfirmStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfirmStep.tsx index f43bdeeb71..0ba54ea78f 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfirmStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfirmStep.tsx @@ -44,7 +44,7 @@ const SSOConfigConfirmStep = () => (

    Select the "Finish" button below or close this form via the - "X" in the upper right corner while you wait for your + {' '}"X" in the upper right corner while you wait for your configuration email. Your SSO testing status will display on the following SSO settings screen.

    diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx index ac33ed1c18..bd0139db9a 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Container, Dropzone, Form } from '@edx/paragon'; +import { Container, Dropzone, Form, Stack } from '@edx/paragon'; import ValidatedFormRadio from '../../../forms/ValidatedFormRadio'; import ValidatedFormControl from '../../../forms/ValidatedFormControl'; @@ -73,9 +73,9 @@ const SSOConfigConnectStep = () => { -

    Let's get started

    - What is your organization's SSO Identity Provider? - +

    Let's get started

    +

    What is your organization's SSO Identity Provider?

    + { /> -

    Connect edX to your Identity Provider

    - Select a method to connect edX to your Identity Provider - +

    Connect edX to your Identity Provider

    +

    Select a method to connect edX to your Identity Provider

    + { {showUrlEntry && ( - - - + + + )} {showXmlUpload diff --git a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx index 6e0fcece64..bfb2c8fc9a 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewExistingSSOConfigs.test.jsx @@ -40,6 +40,8 @@ const inactiveConfig = [ configured_at: '2022-05-12T19:51:25Z', validated_at: '2022-06-12T19:51:25Z', submitted_at: '2022-04-12T19:51:25Z', + is_pending_configuration: false, + errored_at: null, }, ]; const activeConfig = [ @@ -51,6 +53,8 @@ const activeConfig = [ configured_at: '2022-05-12T19:51:25Z', validated_at: '2022-06-12T19:51:25Z', submitted_at: '2022-04-12T19:51:25Z', + is_pending_configuration: false, + errored_at: null, }, ]; const unvalidatedConfig = [ @@ -62,6 +66,8 @@ const unvalidatedConfig = [ configured_at: '2022-04-12T19:51:25Z', validated_at: null, submitted_at: '2022-04-12T19:51:25Z', + is_pending_configuration: false, + errored_at: null, }, ]; const inProgressConfig = [ @@ -73,6 +79,8 @@ const inProgressConfig = [ configured_at: '2021-04-12T19:51:25Z', validated_at: null, submitted_at: '2022-04-12T19:51:25Z', + is_pending_configuration: true, + errored_at: null, }, ]; const notConfiguredConfig = [ @@ -84,6 +92,32 @@ const notConfiguredConfig = [ configured_at: null, validated_at: null, submitted_at: '2022-04-12T19:51:25Z', + is_pending_configuration: true, + errored_at: null, + }, +]; +const timedOutConfig = [ + { + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + active: false, + modified: '2022-04-12T19:51:25Z', + configured_at: null, + validated_at: null, + submitted_at: '2022-04-12T19:51:25Z', + is_pending_configuration: false, + errored_at: null, + }, +]; +const erroredConfig = [ + { + uuid: 'ecc16800-c1cc-4cdb-93aa-186f71b026ca', + active: false, + modified: null, + configured_at: null, + validated_at: null, + submitted_at: '2022-04-10T19:51:25Z', + is_pending_configuration: false, + errored_at: '2022-04-12T19:51:25Z', }, ]; @@ -331,4 +365,38 @@ describe('New Existing SSO Configs tests', () => { await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); expect(mockSetPollingNetworkError).toHaveBeenCalledTimes(1); }); + test('detects timed out configs', async () => { + const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords'); + spy.mockImplementation(() => Promise.resolve({ + data: timedOutConfig, + })); + setupNewExistingSSOConfigs(timedOutConfig); + await waitFor(() => expect( + screen.queryByText( + 'SSO Configuration timed out', + ), + ).toBeInTheDocument()); + const button = screen.getByTestId('sso-timeout-alert-configure'); + act(() => { + userEvent.click(button); + }); + expect(mockSetProviderConfig).toHaveBeenCalledTimes(1); + }); + test('detects errored configs', async () => { + const spy = jest.spyOn(LmsApiService, 'listEnterpriseSsoOrchestrationRecords'); + spy.mockImplementation(() => Promise.resolve({ + data: erroredConfig, + })); + setupNewExistingSSOConfigs(erroredConfig); + await waitFor(() => expect( + screen.queryByText( + 'SSO Configuration failed', + ), + ).toBeInTheDocument()); + const button = screen.getByTestId('sso-errored-alert-configure'); + act(() => { + userEvent.click(button); + }); + expect(mockSetProviderConfig).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx index 1ca8978463..150114538f 100644 --- a/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx +++ b/src/components/settings/SettingsSSOTab/tests/NewSSOConfigAlerts.test.jsx @@ -58,6 +58,9 @@ describe('New SSO Config Alerts Tests', () => { untestedConfigs={[{ display_name: 'untested' }]} notConfigured={[]} closeAlerts={jest.fn()} + timedOutConfigs={[]} + erroredConfigs={[]} + setIsStepperOpen={jest.fn()} />, @@ -90,6 +93,9 @@ describe('New SSO Config Alerts Tests', () => { untestedConfigs={[]} notConfigured={[{ display_name: 'not configured' }]} closeAlerts={jest.fn()} + timedOutConfigs={[]} + erroredConfigs={[]} + setIsStepperOpen={jest.fn()} />, @@ -113,6 +119,9 @@ describe('New SSO Config Alerts Tests', () => { untestedConfigs={[{ display_name: 'untested' }]} notConfigured={[]} closeAlerts={jest.fn()} + timedOutConfigs={[]} + erroredConfigs={[]} + setIsStepperOpen={jest.fn()} />, @@ -150,6 +159,9 @@ describe('New SSO Config Alerts Tests', () => { untestedConfigs={[]} notConfigured={[]} closeAlerts={jest.fn()} + timedOutConfigs={[]} + erroredConfigs={[]} + setIsStepperOpen={jest.fn()} />, @@ -173,6 +185,9 @@ describe('New SSO Config Alerts Tests', () => { untestedConfigs={[{ display_name: 'untested' }]} notConfigured={[]} closeAlerts={mockCloseAlerts} + timedOutConfigs={[]} + erroredConfigs={[]} + setIsStepperOpen={jest.fn()} />, @@ -199,6 +214,9 @@ describe('New SSO Config Alerts Tests', () => { untestedConfigs={[]} notConfigured={[]} closeAlerts={jest.fn()} + timedOutConfigs={[]} + erroredConfigs={[]} + setIsStepperOpen={jest.fn()} />, diff --git a/src/components/settings/SettingsSSOTab/utils.js b/src/components/settings/SettingsSSOTab/utils.js index e9fd44a082..b5d0d8f7d5 100644 --- a/src/components/settings/SettingsSSOTab/utils.js +++ b/src/components/settings/SettingsSSOTab/utils.js @@ -31,6 +31,10 @@ function isInProgressConfig(config) { || config.configured_at < config.submitted_at; } +function checkErroredOrTimedOutConfig(config) { + return config.errored_at || (config.submitted_at && !config.configured_at && !config.is_pending_configuration); +} + export { - updateSamlProviderData, deleteSamlProviderData, createSAMLURLs, isInProgressConfig, + updateSamlProviderData, deleteSamlProviderData, createSAMLURLs, isInProgressConfig, checkErroredOrTimedOutConfig, }; diff --git a/src/components/settings/settings.scss b/src/components/settings/settings.scss index 99079b1b41..9730540a88 100644 --- a/src/components/settings/settings.scss +++ b/src/components/settings/settings.scss @@ -14,6 +14,10 @@ max-height: 71px; } +.sso-alert-width { + width: 73% !important; +} + .lms-card-hover { &:hover { box-shadow: $box-shadow; From b42132d21328968b6213886cbe85397b02ed7f35 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 7 Dec 2023 15:38:23 -0500 Subject: [PATCH 103/124] fix: modifies product tours state to fix re-rendering error (#1128) * fix: modifies product tours state to fix re-rendering error * chore: useMemo to address linting errors * chore: fix more linting errors * fix: return stale time to original value * fix: remove unnecessary useState * fix: remove unnecessary useState * fix: consolidate logic of Product around removing the useState --- .../EnterpriseSubsidiesContext/index.jsx | 1 + src/components/ProductTours/ProductTours.jsx | 8 ++++---- src/components/ProductTours/data/hooks.js | 20 +++++++------------ 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/EnterpriseSubsidiesContext/index.jsx b/src/components/EnterpriseSubsidiesContext/index.jsx index 116d494c44..82d3f9612b 100644 --- a/src/components/EnterpriseSubsidiesContext/index.jsx +++ b/src/components/EnterpriseSubsidiesContext/index.jsx @@ -18,6 +18,7 @@ export const useEnterpriseSubsidiesContext = ({ enterpriseId, isTopDownAssignmentEnabled, }); + const { budgets = [], canManageLearnerCredit = false, diff --git a/src/components/ProductTours/ProductTours.jsx b/src/components/ProductTours/ProductTours.jsx index af76826796..901770cf2c 100644 --- a/src/components/ProductTours/ProductTours.jsx +++ b/src/components/ProductTours/ProductTours.jsx @@ -32,10 +32,10 @@ const ProductTours = ({ const enablePortalAppearance = features.SETTINGS_PAGE_APPEARANCE_TAB; const history = useHistory(); const enabledFeatures = { - [PORTAL_APPEARANCE_TOUR_COOKIE_NAME]: usePortalAppearanceTour({ enablePortalAppearance })[0], - [BROWSE_AND_REQUEST_TOUR_COOKIE_NAME]: useBrowseAndRequestTour({ enableLearnerPortal })[0], - [LEARNER_CREDIT_COOKIE_NAME]: useLearnerCreditTour()[0], - [HIGHLIGHTS_COOKIE_NAME]: useHighlightsTour(FEATURE_CONTENT_HIGHLIGHTS)[0], + [PORTAL_APPEARANCE_TOUR_COOKIE_NAME]: usePortalAppearanceTour({ enablePortalAppearance }), + [BROWSE_AND_REQUEST_TOUR_COOKIE_NAME]: useBrowseAndRequestTour({ enableLearnerPortal }), + [LEARNER_CREDIT_COOKIE_NAME]: useLearnerCreditTour(), + [HIGHLIGHTS_COOKIE_NAME]: useHighlightsTour(FEATURE_CONTENT_HIGHLIGHTS), }; const newFeatureTourCheckpoints = { [PORTAL_APPEARANCE_TOUR_COOKIE_NAME]: portalAppearanceTour({ diff --git a/src/components/ProductTours/data/hooks.js b/src/components/ProductTours/data/hooks.js index daff89f8b1..0c01b946a6 100644 --- a/src/components/ProductTours/data/hooks.js +++ b/src/components/ProductTours/data/hooks.js @@ -1,4 +1,4 @@ -import { useContext, useState } from 'react'; +import { useContext } from 'react'; import { BROWSE_AND_REQUEST_TOUR_COOKIE_NAME, PORTAL_APPEARANCE_TOUR_COOKIE_NAME, @@ -9,11 +9,10 @@ import { SubsidyRequestsContext } from '../../subsidy-requests'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; export const usePortalAppearanceTour = ({ enablePortalAppearance }) => { - const dismissedLearnerCreditTourCookie = global.localStorage.getItem(PORTAL_APPEARANCE_TOUR_COOKIE_NAME); + const dismissedPortalAppearanceTourCookie = global.localStorage.getItem(PORTAL_APPEARANCE_TOUR_COOKIE_NAME); // Only show tour if feature is enabled, hide cookie is undefined or false or not in the settings page - const showPortalAppearanceTour = enablePortalAppearance && !dismissedLearnerCreditTourCookie; - const [portalAppearanceTourEnabled, setPortalAppearanceTourEnabled] = useState(showPortalAppearanceTour); - return [portalAppearanceTourEnabled, setPortalAppearanceTourEnabled]; + const showPortalAppearanceTour = enablePortalAppearance && !dismissedPortalAppearanceTourCookie; + return showPortalAppearanceTour; }; export const useBrowseAndRequestTour = ({ @@ -25,9 +24,7 @@ export const useBrowseAndRequestTour = ({ // not in settings page, and subsidy requests are not already enabled const showBrowseAndRequestTour = enableLearnerPortal && enterpriseSubsidyTypesForRequests.length > 0 && !dismissedBrowseAndRequestTourCookie && !subsidyRequestConfiguration?.subsidyRequestsEnabled; - - const [browseAndRequestTourEnabled, setBrowseAndRequestTourEnabled] = useState(showBrowseAndRequestTour); - return [browseAndRequestTourEnabled, setBrowseAndRequestTourEnabled]; + return showBrowseAndRequestTour; }; export const useLearnerCreditTour = () => { @@ -36,15 +33,12 @@ export const useLearnerCreditTour = () => { // Only show tour if feature is enabled, the enterprise is eligible for the feature, // hide cookie is undefined or false, not in learner credit page const showLearnerCreditTour = canManageLearnerCredit && !dismissedLearnerCreditTourCookie; - - const [learnerCreditTourEnabled, setBrowseAndRequestTourEnabled] = useState(showLearnerCreditTour); - return [learnerCreditTourEnabled, setBrowseAndRequestTourEnabled]; + return showLearnerCreditTour; }; export const useHighlightsTour = (enableHighlights) => { const dismissedHighlightsTourCookie = global.localStorage.getItem(HIGHLIGHTS_COOKIE_NAME); // Only show tour if feature is enabled, hide cookie is undefined or false or not in the settings page const showHighlightsTour = enableHighlights && !dismissedHighlightsTourCookie; - const [highlightsTourEnabled, setHighlightsTourEnabled] = useState(showHighlightsTour); - return [highlightsTourEnabled, setHighlightsTourEnabled]; + return showHighlightsTour; }; From c5d44f099d1bc762b058a026166d9c8e3e89b702 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Fri, 8 Dec 2023 11:37:42 -0500 Subject: [PATCH 104/124] fix: adds sane fallback to if learnerState is failed and no error reason exist (#1129) * fix: adds sane fallback to if learnerState is failed and no error reason exist * fix: update comments * chore: PR fixes * chore: testing --- .../AssignmentStatusTableCell.jsx | 8 ++++++-- .../tests/BudgetDetailPage.test.jsx | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx index 10cb79caf9..af24ed54fb 100644 --- a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx @@ -8,6 +8,7 @@ import FailedReminder from './assignments-status-chips/FailedReminder'; import FailedSystem from './assignments-status-chips/FailedSystem'; import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; import WaitingForLearner from './assignments-status-chips/WaitingForLearner'; +import { capitalizeFirstLetter } from '../../utils'; const AssignmentStatusTableCell = ({ row }) => { const { original } = row; @@ -16,7 +17,6 @@ const AssignmentStatusTableCell = ({ row }) => { learnerState, errorReason, } = original; - // Learner state is not available for this assignment, so don't display anything. if (!learnerState) { return null; @@ -36,6 +36,10 @@ const AssignmentStatusTableCell = ({ row }) => { } if (learnerState === 'failed') { + // If learnerState is failed but no error reason is defined, return a failed system chip. + if (!errorReason) { + return ; + } // Determine which failure chip to display based on the error reason. if (errorReason.actionType === 'notified') { if (errorReason.errorReason === 'email_error') { @@ -54,7 +58,7 @@ const AssignmentStatusTableCell = ({ row }) => { } // Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway. - return {`${learnerState.charAt(0).toUpperCase()}${learnerState.substr(1)}`}; + return {`${capitalizeFirstLetter(learnerState)}`}; }; AssignmentStatusTableCell.propTypes = { diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index aee974de7c..b477212f03 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -846,6 +846,15 @@ describe('', () => { actionType: 'notified', }, }, + { + learnerState: 'failed', + hasLearnerEmail: true, + expectedChipStatus: 'Failed: System', + expectedModalPopupHeading: 'Failed: System', + expectedModalPopupContent: 'Something went wrong behind the scenes.', + actions: [mockFailedLinkedLearnerAction], + errorReason: null, + }, { learnerState: 'failed', hasLearnerEmail: true, From 383319b3928aad4e32421b2267abef4328bd7041 Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Wed, 6 Dec 2023 17:30:34 -0800 Subject: [PATCH 105/124] feat: use configurable URL in cards/chips within the learner-credit-management tool ENT-7907 --- .env.development | 3 +++ .../learner-credit-management/BudgetDetailAssignments.jsx | 7 ++++++- .../learner-credit-management/BudgetDetailRedemptions.jsx | 7 ++++++- .../assignments-status-chips/FailedBadEmail.jsx | 3 ++- .../assignments-status-chips/FailedCancellation.jsx | 4 +++- .../assignments-status-chips/FailedSystem.jsx | 3 ++- .../assignments-status-chips/WaitingForLearner.jsx | 3 ++- .../cards/NewAssignmentModalButton.jsx | 5 +++-- src/index.jsx | 1 + 9 files changed, 28 insertions(+), 8 deletions(-) diff --git a/.env.development b/.env.development index 92e64b6562..c63505b9aa 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,4 @@ +APP_ID='admin-portal' NODE_ENV='development' BASE_URL='http://localhost:1991' LMS_BASE_URL='http://localhost:18000' @@ -17,6 +18,7 @@ ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734' ENTERPRISE_SUPPORT_URL='https://edx.org' ENTERPRISE_SUPPORT_REVOKE_LICENSE_URL='https://edx.org' ENTERPRISE_SUPPORT_PROGRAM_OPTIMIZATION_URL='https://edx.org' +ENTERPRISE_SUPPORT_LEARNER_CREDIT_URL='https://edx.org' SEGMENT_KEY='' ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' USER_INFO_COOKIE_NAME='edx-user-info' @@ -53,3 +55,4 @@ USE_API_CACHE='true' SUBSCRIPTION_LPR='true' PLOTLY_SERVER_URL='http://localhost:8050' AUTH0_SELF_SERVICE_INTEGRATION='true' +MFE_CONFIG_API_URL='http://localhost:18000/api/mfe_config/v1' diff --git a/src/components/learner-credit-management/BudgetDetailAssignments.jsx b/src/components/learner-credit-management/BudgetDetailAssignments.jsx index b7caecce6b..41bcbe1275 100644 --- a/src/components/learner-credit-management/BudgetDetailAssignments.jsx +++ b/src/components/learner-credit-management/BudgetDetailAssignments.jsx @@ -1,6 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { Hyperlink } from '@edx/paragon'; +import { getConfig } from '@edx/frontend-platform/config'; import BudgetAssignmentsTable from './BudgetAssignmentsTable'; import AssignMoreCoursesEmptyStateMinimal from './AssignMoreCoursesEmptyStateMinimal'; @@ -42,7 +44,10 @@ const BudgetDetailAssignments = ({

    Assigned

    Assigned activity earmarks funds in your budget so you can't overspend. For funds to move - from assigned to spent, your learners must complete enrollment. + from assigned to spent, your learners must complete enrollment.{' '} + + Learn more +

    {

    Spent

    - Spent activity is driven by completed enrollments. + Spent activity is driven by completed enrollments.{' '} + + Learn more + {(enterpriseOfferId || (subsidyAccessPolicyId && !enterpriseFeatures.topDownAssignmentRealTimeLcm)) && ( <> Enrollment data is automatically updated every 12 hours. diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx index 22228de922..b6664a8768 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Chip, Hyperlink, useToggle } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; @@ -40,7 +41,7 @@ const FailedBadEmail = ({ learnerEmail }) => {

  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx index 182e91df7b..a46a4f69d5 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import { Chip, useToggle, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; + import BaseModalPopup from './BaseModalPopup'; const FailedCancellation = () => { @@ -41,7 +43,7 @@ const FailedCancellation = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments
  • diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx index cfee2cb7bb..43ae45e4b0 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Chip, Hyperlink, useToggle } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; @@ -37,7 +38,7 @@ const FailedSystem = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • diff --git a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx index 50a06dc522..f1176b3317 100644 --- a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Chip, Hyperlink, useToggle } from '@edx/paragon'; import { Timelapse } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; import { ASSIGNMENT_ENROLLMENT_DEADLINE } from '../data'; @@ -39,7 +40,7 @@ const WaitingForLearner = ({ learnerEmail }) => {

    Need help?

    Learn more about learner enrollment in assigned courses at{' '} - + Help Center: Course Assignments .

    diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 47a3899735..eb2a089129 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -12,8 +12,9 @@ import { import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform/utils'; - import { connect } from 'react-redux'; +import { getConfig } from '@edx/frontend-platform/config'; + import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy } from '../data'; @@ -196,7 +197,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_HELP_CENTER, )} - destination="https://edx.org" + destination={getConfig().ENTERPRISE_SUPPORT_LEARNER_CREDIT_URL} showLaunchIcon target="_blank" > diff --git a/src/index.jsx b/src/index.jsx index 39bebbc913..dc5c5c07f2 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -39,6 +39,7 @@ initialize({ FEATURE_LEARNER_CREDIT_MANAGEMENT: process.env.FEATURE_LEARNER_CREDIT_MANAGEMENT || hasFeatureFlagEnabled('LEARNER_CREDIT_MANAGEMENT') || null, FEATURE_CONTENT_HIGHLIGHTS: process.env.FEATURE_CONTENT_HIGHLIGHTS || hasFeatureFlagEnabled('CONTENT_HIGHLIGHTS') || null, ENTERPRISE_SUPPORT_PROGRAM_OPTIMIZATION_URL: process.env.ENTERPRISE_SUPPORT_PROGRAM_OPTIMIZATION_URL || null, + ENTERPRISE_SUPPORT_LEARNER_CREDIT_URL: process.env.ENTERPRISE_SUPPORT_LEARNER_CREDIT_URL || null, }); }, }, From 7b3ea4b0b6b6f4dbc93d4e6a9859df03b8136aea Mon Sep 17 00:00:00 2001 From: Cyril Nxumalo <80963114+zwidekalanga@users.noreply.github.com> Date: Fri, 8 Dec 2023 21:25:04 +0200 Subject: [PATCH 106/124] feat: [Budget Detail Header] Display aggregates and budget overview (#1110) Co-authored-by: Adam Stankiewicz --- .../learner-credit-management/BudgetCard.jsx | 2 +- .../BudgetDetailAssignments.jsx | 19 +- .../BudgetDetailCatalogTabContents.jsx | 20 +- .../BudgetDetailPageBreadcrumbs.jsx | 31 +++ .../BudgetDetailPageHeader.jsx | 124 ++++++++--- .../BudgetDetailPageOverviewAvailability.jsx | 120 +++++++++++ .../BudgetDetailPageOverviewUtilization.jsx | 127 ++++++++++++ .../BudgetDetailRedemptions.jsx | 19 +- .../data/constants.js | 2 + .../data/hooks/index.js | 2 + .../hooks/tests/useEnterpriseOffer.test.jsx | 100 +++++++++ .../useSubsidySummaryAnalyticsApi.test.js | 3 +- .../data/hooks/useBudgetDetailHeaderData.js | 73 +++++++ .../data/hooks/useEnterpriseOffer.js | 20 ++ .../hooks/useSubsidySummaryAnalyticsApi.js | 8 +- .../data/tests/constants.js | 97 +++++++++ .../search/CatalogSearch.jsx | 2 +- .../styles/index.scss | 5 +- .../tests/BudgetDetailPage.test.jsx | 196 +++++++++++++++++- src/data/services/EcommerceApiService.js | 10 + 20 files changed, 933 insertions(+), 47 deletions(-) create mode 100644 src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx create mode 100644 src/components/learner-credit-management/data/hooks/tests/useEnterpriseOffer.test.jsx create mode 100644 src/components/learner-credit-management/data/hooks/useBudgetDetailHeaderData.js create mode 100644 src/components/learner-credit-management/data/hooks/useEnterpriseOffer.js diff --git a/src/components/learner-credit-management/BudgetCard.jsx b/src/components/learner-credit-management/BudgetCard.jsx index 19d70f79a1..b3dbaf470c 100644 --- a/src/components/learner-credit-management/BudgetCard.jsx +++ b/src/components/learner-credit-management/BudgetCard.jsx @@ -25,7 +25,7 @@ const BudgetCard = ({ const { isLoading: isLoadingSubsidySummaryAnalyticsApi, subsidySummary: subsidySummaryAnalyticsApi, - } = useSubsidySummaryAnalyticsApi(enterpriseUUID, budget); + } = useSubsidySummaryAnalyticsApi(enterpriseUUID, budget.id, budget.source); // Subsidy Access Policies will always have a single budget, so we can render a single card // without relying on `useSubsidySummaryAnalyticsApi`. diff --git a/src/components/learner-credit-management/BudgetDetailAssignments.jsx b/src/components/learner-credit-management/BudgetDetailAssignments.jsx index 41bcbe1275..99a367ac04 100644 --- a/src/components/learner-credit-management/BudgetDetailAssignments.jsx +++ b/src/components/learner-credit-management/BudgetDetailAssignments.jsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Hyperlink } from '@edx/paragon'; import { getConfig } from '@edx/frontend-platform/config'; +import { useHistory } from 'react-router-dom'; import BudgetAssignmentsTable from './BudgetAssignmentsTable'; import AssignMoreCoursesEmptyStateMinimal from './AssignMoreCoursesEmptyStateMinimal'; import { useBudgetContentAssignments, useBudgetId, useSubsidyAccessPolicy } from './data'; @@ -14,8 +15,13 @@ const BudgetDetailAssignments = ({ enterpriseFeatures, enterpriseId, }) => { + const assignedHeadingRef = useRef(); const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const history = useHistory(); + + const { location } = history; + const { state: locationState } = location; const isAssignableBudget = !!subsidyAccessPolicy?.isAssignable; const assignmentConfigurationUUID = subsidyAccessPolicy?.assignmentConfiguration?.uuid; const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm; @@ -29,6 +35,15 @@ const BudgetDetailAssignments = ({ enterpriseId, }); + useEffect(() => { + if (locationState?.budgetActivityScrollToKey === 'assigned') { + assignedHeadingRef.current?.scrollIntoView({ behavior: 'smooth' }); + const newState = { ...locationState }; + delete newState.budgetActivityScrollToKey; + history.replace({ ...location, state: newState }); + } + }, [history, location, locationState]); + if (!isTopDownAssignmentEnabled || !isAssignableBudget) { return null; } @@ -41,7 +56,7 @@ const BudgetDetailAssignments = ({ return (
    -

    Assigned

    +

    Assigned

    Assigned activity earmarks funds in your budget so you can't overspend. For funds to move from assigned to spent, your learners must complete enrollment.{' '} diff --git a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx index ee67ecb6de..af044e0a0f 100644 --- a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { InstantSearch } from 'react-instantsearch-dom'; import algoliasearch from 'algoliasearch/lite'; import { Row, Col } from '@edx/paragon'; import { SearchData, SEARCH_FACET_FILTERS } from '@edx/frontend-enterprise-catalog-search'; +import { useHistory } from 'react-router'; import CatalogSearch from './search/CatalogSearch'; import { LANGUAGE_REFINEMENT, @@ -12,6 +13,11 @@ import { import { configuration } from '../../config'; const BudgetDetailCatalogTabContents = () => { + const history = useHistory(); + const { location } = history; + const { state: locationState } = location; + const catalogContainerRef = useRef(); + const language = { attribute: LANGUAGE_REFINEMENT, title: 'Language', @@ -31,8 +37,18 @@ const BudgetDetailCatalogTabContents = () => { configuration.ALGOLIA.APP_ID, configuration.ALGOLIA.SEARCH_API_KEY, ); + + useEffect(() => { + if (locationState?.budgetActivityScrollToKey === 'catalog') { + catalogContainerRef.current?.scrollIntoView({ behavior: 'smooth' }); + const newState = { ...locationState }; + delete newState.budgetActivityScrollToKey; + history.replace({ ...location, state: newState }); + } + }, [history, location, locationState]); + return ( - + ( +

    + +
    +); + +const mapStateToProps = state => ({ + enterpriseSlug: state.portalConfiguration.enterpriseSlug, +}); + +BudgetDetailPageBreadcrumbs.propTypes = { + enterpriseSlug: PropTypes.string.isRequired, + budgetDisplayName: PropTypes.string.isRequired, +}; + +export default connect(mapStateToProps)(BudgetDetailPageBreadcrumbs); diff --git a/src/components/learner-credit-management/BudgetDetailPageHeader.jsx b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx index ad84c6724e..2ea4b409e1 100644 --- a/src/components/learner-credit-management/BudgetDetailPageHeader.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx @@ -1,50 +1,114 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; import { - Row, Col, Breadcrumb, Stack, + Stack, Card, Badge, Skeleton, } from '@edx/paragon'; -import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; -import { useBudgetId, useSubsidyAccessPolicy } from './data'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + useBudgetId, + useSubsidyAccessPolicy, + useBudgetDetailHeaderData, + useEnterpriseOffer, + formatDate, + useSubsidySummaryAnalyticsApi, +} from './data'; + +import BudgetDetailPageBreadcrumbs from './BudgetDetailPageBreadcrumbs'; +import BudgetDetailPageOverviewAvailability from './BudgetDetailPageOverviewAvailability'; +import BudgetDetailPageOverviewUtilization from './BudgetDetailPageOverviewUtilization'; +import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; + +const BudgetStatusBadge = ({ + badgeVariant, status, term, date, +}) => ( + + { status } + {term} { formatDate(date) } + +); + +BudgetStatusBadge.propTypes = { + badgeVariant: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + term: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, +}; -const BudgetDetailPageHeader = ({ enterpriseSlug }) => { - const { subsidyAccessPolicyId } = useBudgetId(); +const BudgetDetailPageHeader = ({ enterpriseUUID }) => { + const { subsidyAccessPolicyId, enterpriseOfferId } = useBudgetId(); + const budgetType = (enterpriseOfferId !== null) ? BUDGET_TYPES.ecommerce : BUDGET_TYPES.policy; + + const { isLoading: isLoadingSubsidySummary, subsidySummary } = useSubsidySummaryAnalyticsApi( + enterpriseUUID, + enterpriseOfferId, + budgetType, + ); + + const { isLoading: isLoadingEnterpriseOffer, data: enterpriseOfferMetadata } = useEnterpriseOffer(enterpriseOfferId); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); - const budgetDisplayName = subsidyAccessPolicy?.displayName || 'Overview'; + + const policyOrOfferId = subsidyAccessPolicyId || enterpriseOfferId; + const { + budgetId, + budgetDisplayName, + budgetTotalSummary, + budgetAggregates, + status, + badgeVariant, + term, + date, + isAssignable, + } = useBudgetDetailHeaderData({ + subsidyAccessPolicy, + subsidySummary, + budgetId: policyOrOfferId, + enterpriseOfferMetadata, + }); + + if (!subsidyAccessPolicy && (isLoadingSubsidySummary || isLoadingEnterpriseOffer)) { + return ( +
    + + Loading budget header data +
    + ); + } + + if (subsidyAccessPolicy === null && subsidySummary === null) { + return null; + } + return ( - - - + + +

    { budgetDisplayName }

    + + + - -
    - {budgetDisplayName && ( - - -

    {budgetDisplayName}

    - -
    - )} + +
    ); }; const mapStateToProps = state => ({ - enterpriseSlug: state.portalConfiguration.enterpriseSlug, + enterpriseUUID: state.portalConfiguration.enterpriseId, }); BudgetDetailPageHeader.propTypes = { - enterpriseSlug: PropTypes.string.isRequired, + enterpriseUUID: PropTypes.string.isRequired, }; export default connect(mapStateToProps)(BudgetDetailPageHeader); diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx new file mode 100644 index 0000000000..761fa1085d --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Button, Col, Hyperlink, ProgressBar, Row, Stack, +} from '@edx/paragon'; +import { Add } from '@edx/paragon/icons'; +import { generatePath, useRouteMatch, Link } from 'react-router-dom'; +import { formatPrice } from './data'; +import { configuration } from '../../config'; + +const BudgetDetail = ({ available, utilized, limit }) => { + const currentProgressBarLimit = (available / limit) * 100; + + return ( + +

    Available

    + + {formatPrice(available)} + + Utilized {formatPrice(utilized)} + + + + + + {formatPrice(limit)} limit + + +
    + ); +}; + +BudgetDetail.propTypes = { + available: PropTypes.number.isRequired, + utilized: PropTypes.number.isRequired, + limit: PropTypes.number.isRequired, +}; + +const BudgetActions = ({ budgetId, isAssignable }) => { + const routeMatch = useRouteMatch(); + const supportUrl = configuration.ENTERPRISE_SUPPORT_URL; + + if (!isAssignable) { + return ( +
    +
    +

    Get people learning using this budget

    +

    + Funds from this budget are set to autoallocate to registered learners based on + settings configured with your support team. +

    + +
    +
    + ); + } + + return ( +
    +
    +

    Get people learning using this budget

    + +
    +
    + ); +}; + +BudgetActions.propTypes = { + budgetId: PropTypes.string.isRequired, + isAssignable: PropTypes.bool.isRequired, +}; + +const BudgetDetailPageOverviewAvailability = ( + { + budgetId, + isAssignable, + budgetTotalSummary: { available, utilized, limit }, + }, +) => ( + + + + + + + + + + +); + +const budgetTotalSummaryShape = { + utilized: PropTypes.number.isRequired, + available: PropTypes.number.isRequired, + limit: PropTypes.number.isRequired, +}; + +BudgetDetailPageOverviewAvailability.propTypes = { + budgetId: PropTypes.string.isRequired, + budgetTotalSummary: PropTypes.shape(budgetTotalSummaryShape).isRequired, + isAssignable: PropTypes.bool.isRequired, +}; + +export default BudgetDetailPageOverviewAvailability; diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx new file mode 100644 index 0000000000..5fcd15022b --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx @@ -0,0 +1,127 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Stack, Collapsible, Row, Col, Button, +} from '@edx/paragon'; +import { ArrowDownward } from '@edx/paragon/icons'; + +import { + generatePath, useRouteMatch, Link, +} from 'react-router-dom'; +import { formatPrice } from './data'; + +const BudgetDetailPageOverviewUtilization = ( + { + budgetId, + budgetTotalSummary: { utilized }, + budgetAggregates, + isAssignable, + }, +) => { + const routeMatch = useRouteMatch(); + + const { amountAllocatedUsd, amountRedeemedUsd } = budgetAggregates; + + if (budgetId === null || utilized <= 0 || !isAssignable) { + return null; + } + + const renderActivityLink = ({ amount, type }) => { + if (amount <= 0) { + return null; + } + + const linkText = (type === 'assigned') ? 'View assigned activity' : 'View spent activity'; + + return ( + + ); + }; + + return ( + Utilization details} + > + + + + +

    Utilized

    + +

    {formatPrice(utilized)}

    +

    + Your total utilization includes both assigned funds (earmarked for future enrollment) and spent + funds (redeemed for enrollment). +

    + + + Amount assigned + + {formatPrice(amountAllocatedUsd)} + + + { + renderActivityLink({ + amount: amountAllocatedUsd, + type: 'assigned', + }) + } + + + + Amount spent + + {formatPrice(amountRedeemedUsd)} + + + { + renderActivityLink({ + amount: amountRedeemedUsd, + type: 'spent', + }) + } + + + +
    +
    + +
    +
    +
    + ); +}; + +const budgetTotalSummaryShape = { + utilized: PropTypes.number.isRequired, + available: PropTypes.number.isRequired, + limit: PropTypes.number.isRequired, +}; + +const budgetAggregatesShape = { + amountAllocatedUsd: PropTypes.number.isRequired, + amountRedeemedUsd: PropTypes.number.isRequired, +}; + +BudgetDetailPageOverviewUtilization.propTypes = { + budgetId: PropTypes.string.isRequired, + budgetTotalSummary: PropTypes.shape(budgetTotalSummaryShape).isRequired, + budgetAggregates: PropTypes.shape(budgetAggregatesShape).isRequired, + isAssignable: PropTypes.bool.isRequired, +}; + +export default BudgetDetailPageOverviewUtilization; diff --git a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx index 54a2c4810f..7fe5d41fe8 100644 --- a/src/components/learner-credit-management/BudgetDetailRedemptions.jsx +++ b/src/components/learner-credit-management/BudgetDetailRedemptions.jsx @@ -1,14 +1,19 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Hyperlink } from '@edx/paragon'; import { getConfig } from '@edx/frontend-platform/config'; +import { useHistory } from 'react-router'; import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; import { useBudgetId, useBudgetRedemptions } from './data'; const BudgetDetailRedemptions = ({ enterpriseFeatures, enterpriseUUID }) => { + const history = useHistory(); + const { location } = history; + const { state: locationState } = location; const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId(); + const spentHeadingRef = useRef(); const { isLoading, budgetRedemptions, @@ -19,9 +24,19 @@ const BudgetDetailRedemptions = ({ enterpriseFeatures, enterpriseUUID }) => { subsidyAccessPolicyId, enterpriseFeatures.topDownAssignmentRealTimeLcm, ); + + useEffect(() => { + if (locationState?.budgetActivityScrollToKey === 'spent') { + spentHeadingRef.current?.scrollIntoView({ behavior: 'smooth' }); + const newState = { ...locationState }; + delete newState.budgetActivityScrollToKey; + history.replace({ ...location, state: newState }); + } + }, [history, location, locationState]); + return (
    -

    Spent

    +

    Spent

    Spent activity is driven by completed enrollments.{' '} diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 004b905118..6d82c02338 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -70,6 +70,8 @@ export const learnerCreditManagementQueryKeys = { all: ['learner-credit-management'], budgets: (enterpriseId) => [...learnerCreditManagementQueryKeys.all, 'budgets', enterpriseId], budget: (budgetId) => [...learnerCreditManagementQueryKeys.all, 'budget', budgetId], + // Used when fetching enterprise offer metadata when viewing the budget detail page for enterprise offer + budgetEnterpriseOffer: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'ecommerce'], budgetActivity: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'activity'], budgetActivityOverview: (budgetId) => [...learnerCreditManagementQueryKeys.budgetActivity(budgetId), 'overview'], }; diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index d979f3867a..63681a4030 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -4,9 +4,11 @@ export { default as useBudgetRedemptions } from './useBudgetRedemptions'; export { default as useBudgetContentAssignments } from './useBudgetContentAssignments'; export { default as useBudgetId } from './useBudgetId'; export { default as useSubsidyAccessPolicy } from './useSubsidyAccessPolicy'; +export { default as useBudgetDetailHeaderData } from './useBudgetDetailHeaderData'; export { default as usePathToCatalogTab } from './usePathToCatalogTab'; export { default as useBudgetDetailActivityOverview } from './useBudgetDetailActivityOverview'; export { default as useIsLargeOrGreater } from './useIsLargeOrGreater'; export { default as useSuccessfulAssignmentToastContextValue } from './useSuccessfulAssignmentToastContextValue'; export { default as useSuccessfulCancellationToastContextValue } from './useSuccessfulCancellationToastContextValue'; export { default as useSuccessfulReminderToastContextValue } from './useSuccessfulReminderToastContextValue'; +export { default as useEnterpriseOffer } from './useEnterpriseOffer'; diff --git a/src/components/learner-credit-management/data/hooks/tests/useEnterpriseOffer.test.jsx b/src/components/learner-credit-management/data/hooks/tests/useEnterpriseOffer.test.jsx new file mode 100644 index 0000000000..a5ed946d48 --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/tests/useEnterpriseOffer.test.jsx @@ -0,0 +1,100 @@ +import { QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; + +import EcommerceApiService from '../../../../../data/services/EcommerceApiService'; +import useEnterpriseOffer from '../useEnterpriseOffer'; // Import the hook +import { queryClient } from '../../../../test/testUtils'; + +const mockEnterpriseOfferUUID = '9af340a9-48de-4d94-976d-e2282b9eb7f3'; + +// Mock the EcommerceApiService +jest.mock('../../../../../data/services/EcommerceApiService', () => ({ + fetchEnterpriseOffer: jest.fn().mockResolvedValue({ + data: { + id: 99511, + startDatetime: '2022-09-01T00:00:00Z', + endDatetime: '2024-09-01T00:00:00Z', + displayName: 'Test enterprise', + // Other properties... + }, + }), +})); + +const wrapper = ({ children }) => ( + {children} +); + +describe('useEnterpriseOffer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch and return enterprise offer (%s)', async () => { + // Mock the policy type in response based on isAssignable + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseOffer(mockEnterpriseOfferUUID), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(false); + expect(result.current.data).toEqual({ + id: 99511, + startDatetime: '2022-09-01T00:00:00Z', + endDatetime: '2024-09-01T00:00:00Z', + displayName: 'Test enterprise', + }); + }); + + it('should handle errors gracefully', async () => { + // Mock an error response from the API + jest.spyOn(EcommerceApiService, 'fetchEnterpriseOffer').mockRejectedValueOnce(new Error('Mock API Error')); + + const { result, waitForNextUpdate } = renderHook( + () => useEnterpriseOffer(mockEnterpriseOfferUUID), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(true); + expect(result.current.error.message).toBe('Mock API Error'); + }); + + it.each([ + { + enterpriseOfferId: undefined, + expectedData: undefined, + }, + { + enterpriseOfferId: mockEnterpriseOfferUUID, + expectedData: { + id: 99511, + startDatetime: '2022-09-01T00:00:00Z', + endDatetime: '2024-09-01T00:00:00Z', + displayName: 'Test enterprise', + // Other expected properties... + }, + }, + ])('should enable/disable the query based on subsidyAccessPolicyId (%s)', async ({ + enterpriseOfferId, + expectedData, + }) => { + const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffer(enterpriseOfferId), { wrapper }); + + if (expectedData !== undefined) { + await waitForNextUpdate(); + expect(result.current.isLoading).toBe(false); + } else { + expect(result.current.isLoading).toBe(true); + } + + expect(result.current.isInitialLoading).toBe(false); + expect(result.current.isError).toBe(false); + expect(result.current.data).toEqual(expectedData); + }); +}); diff --git a/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js b/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js index 3fa26919c9..b079df60a9 100644 --- a/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js +++ b/src/components/learner-credit-management/data/hooks/tests/useSubsidySummaryAnalyticsApi.test.js @@ -88,7 +88,8 @@ describe('useSubsidySummaryAnalyticsApi', () => { waitForNextUpdate, } = renderHook(() => useSubsidySummaryAnalyticsApi( TEST_ENTERPRISE_UUID, - mockBudget, + mockBudget.id, + mockBudget.source, )); if (shouldCallApi) { diff --git a/src/components/learner-credit-management/data/hooks/useBudgetDetailHeaderData.js b/src/components/learner-credit-management/data/hooks/useBudgetDetailHeaderData.js new file mode 100644 index 0000000000..06047d3e1f --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useBudgetDetailHeaderData.js @@ -0,0 +1,73 @@ +import { getBudgetStatus } from '../utils'; + +const transformSubsidySummaryToPolicy = (subsidySummary, metadata) => { + if (!subsidySummary) { return null; } + + return { + displayName: metadata.displayName, + subsidyActiveDatetime: metadata.startDatetime, + subsidyExpirationDatetime: metadata.endDatetime, + aggregates: { + spendAvailableUsd: subsidySummary.remainingBalance, + amountAllocatedUsd: 0, + amountRedeemedUsd: subsidySummary.amountOfOfferSpent, + }, + spendLimit: subsidySummary.maxDiscount * 100, + isAssignable: false, + }; +}; + +const assignBudgetStatus = (policy) => { + const { + status, badgeVariant, term, date, + } = getBudgetStatus( + policy.subsidyActiveDatetime, + policy.subsidyExpirationDatetime, + ); + + return { + status, badgeVariant, term, date, + }; +}; + +const assignBudgetDetails = (policy) => { + const { spendAvailableUsd, amountAllocatedUsd, amountRedeemedUsd } = policy.aggregates; + + const available = spendAvailableUsd; + const limit = policy.spendLimit / 100; + const utilized = policy.isAssignable + ? (amountAllocatedUsd + amountRedeemedUsd) + : amountRedeemedUsd; + + return { budgetTotalSummary: { available, limit, utilized } }; +}; + +const useBudgetDetailHeaderData = ({ + subsidyAccessPolicy, subsidySummary, budgetId, enterpriseOfferMetadata, +}) => { + const policy = subsidyAccessPolicy || transformSubsidySummaryToPolicy(subsidySummary, enterpriseOfferMetadata); + + if (policy == null) { + return {}; + } + + const defaultData = { + budgetId, + budgetTotalSummary: { + available: 0, + utilized: 0, + limit: 0, + }, + budgetAggregates: policy.aggregates || {}, + isAssignable: policy.isAssignable || false, + budgetDisplayName: policy.displayName || 'Overview', + }; + + return { + ...defaultData, + ...assignBudgetStatus(policy), + ...assignBudgetDetails(policy), + }; +}; + +export default useBudgetDetailHeaderData; diff --git a/src/components/learner-credit-management/data/hooks/useEnterpriseOffer.js b/src/components/learner-credit-management/data/hooks/useEnterpriseOffer.js new file mode 100644 index 0000000000..453e27843f --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useEnterpriseOffer.js @@ -0,0 +1,20 @@ +// modify the query keys map to include a queryKey for `budgetEnterpriseOffer` that depends on `.budget()`. +import { useQuery } from '@tanstack/react-query'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; +import EcommerceApiService from '../../../../data/services/EcommerceApiService'; +import { learnerCreditManagementQueryKeys } from '../constants'; + +const getEnterpriseOffer = async ({ queryKey }) => { + const enterpriseOfferId = queryKey[2]; + const response = await EcommerceApiService.fetchEnterpriseOffer(enterpriseOfferId); + return camelCaseObject(response.data); +}; + +// Hook to fetch an individual enterprise offer from ecommerce. +const useEnterpriseOffer = (enterpriseOfferId) => useQuery({ + queryKey: learnerCreditManagementQueryKeys.budgetEnterpriseOffer(enterpriseOfferId), + queryFn: getEnterpriseOffer, + enabled: !!enterpriseOfferId, +}); + +export default useEnterpriseOffer; diff --git a/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js b/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js index f0aaee7826..9b559817a2 100644 --- a/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js +++ b/src/components/learner-credit-management/data/hooks/useSubsidySummaryAnalyticsApi.js @@ -6,14 +6,14 @@ import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataAp import { transformSubsidySummary } from '../utils'; import { BUDGET_TYPES } from '../../../EnterpriseApp/data/constants'; -const useSubsidySummaryAnalyticsApi = (enterpriseUUID, budget) => { +const useSubsidySummaryAnalyticsApi = (enterpriseUUID, budgetId, budgetSource) => { const [isLoading, setIsLoading] = useState(true); const [subsidySummary, setSubsidySummary] = useState(); useEffect(() => { // If there is no budget, or the budget is NOT an ecommerce offer or subsidy, fetch the // subsidy summary data from the analytics API. - if (![BUDGET_TYPES.ecommerce, BUDGET_TYPES.subsidy].includes(budget?.source)) { + if (![BUDGET_TYPES.ecommerce, BUDGET_TYPES.subsidy].includes(budgetSource)) { setIsLoading(false); return; } @@ -23,7 +23,7 @@ const useSubsidySummaryAnalyticsApi = (enterpriseUUID, budget) => { setIsLoading(true); const response = await EnterpriseDataApiService.fetchEnterpriseOfferSummary( enterpriseUUID, - budget.id, + budgetId, ); const data = camelCaseObject(response.data); const transformedSubsidySummary = transformSubsidySummary(data); @@ -36,7 +36,7 @@ const useSubsidySummaryAnalyticsApi = (enterpriseUUID, budget) => { }; fetchData(); - }, [enterpriseUUID, budget]); + }, [enterpriseUUID, budgetId, budgetSource]); return { isLoading, diff --git a/src/components/learner-credit-management/data/tests/constants.js b/src/components/learner-credit-management/data/tests/constants.js index 285a0e7e82..69503fbc46 100644 --- a/src/components/learner-credit-management/data/tests/constants.js +++ b/src/components/learner-credit-management/data/tests/constants.js @@ -3,13 +3,75 @@ export const mockSubsidyAccessPolicyUUID = 'c17de32e-b80b-468f-b994-85e68fd32751 export const mockAssignableSubsidyAccessPolicy = { uuid: mockSubsidyAccessPolicyUUID, + subsidyActiveDatetime: '2023-11-01T13:06:46Z', + subsidyExpirationDatetime: '2024-02-29T13:06:59Z', policyType: 'AssignedLearnerCreditAccessPolicy', + displayName: 'Assignable Learner Credit', + spendLimit: 10000 * 100, assignmentConfiguration: { uuid: 'test-uuid', }, + aggregates: { + spendAvailableUsd: 10000, + amountAllocatedUsd: 100, + amountRedeemedUsd: 350, + }, + isAssignable: true, + subsidyUuid: 'mock-subsidy-uuid', +}; + +export const mockAssignableSubsidyAccessPolicyWithNoUtilization = { + uuid: mockSubsidyAccessPolicyUUID, + subsidyActiveDatetime: '2023-11-01T13:06:46Z', + subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + policyType: 'AssignedLearnerCreditAccessPolicy', displayName: 'Assignable Learner Credit', + spendLimit: 10000 * 100, + assignmentConfiguration: { + uuid: 'test-uuid', + }, aggregates: { spendAvailableUsd: 10000, + amountAllocatedUsd: 0, + amountRedeemedUsd: 0, + }, + isAssignable: true, + subsidyUuid: 'mock-subsidy-uuid', +}; + +export const mockAssignableSubsidyAccessPolicyWithSpendNoAllocations = { + uuid: mockSubsidyAccessPolicyUUID, + subsidyActiveDatetime: '2023-11-01T13:06:46Z', + subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + policyType: 'AssignedLearnerCreditAccessPolicy', + displayName: 'Assignable Learner Credit', + spendLimit: 10000 * 100, + assignmentConfiguration: { + uuid: 'test-uuid', + }, + aggregates: { + spendAvailableUsd: 10000, + amountAllocatedUsd: 0, + amountRedeemedUsd: 5000, + }, + isAssignable: true, + subsidyUuid: 'mock-subsidy-uuid', +}; + +export const mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed = { + uuid: mockSubsidyAccessPolicyUUID, + subsidyActiveDatetime: '2023-11-01T13:06:46Z', + subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + policyType: 'AssignedLearnerCreditAccessPolicy', + displayName: 'Assignable Learner Credit', + spendLimit: 10000 * 100, + assignmentConfiguration: { + uuid: 'test-uuid', + }, + aggregates: { + spendAvailableUsd: 10000, + amountAllocatedUsd: 0, + amountRedeemedUsd: 5000, }, isAssignable: true, subsidyUuid: 'mock-subsidy-uuid', @@ -17,11 +79,46 @@ export const mockAssignableSubsidyAccessPolicy = { export const mockPerLearnerSpendLimitSubsidyAccessPolicy = { uuid: mockSubsidyAccessPolicyUUID, + subsidyActiveDatetime: '2023-11-01T13:06:46Z', + subsidyExpirationDatetime: '2024-02-29T13:06:59Z', policyType: 'PerLearnerSpendCreditAccessPolicy', displayName: 'Per Learner Spend Limit', + spendLimit: 10000 * 100, aggregates: { spendAvailableUsd: 10000, + amountAllocatedUsd: 100, + amountRedeemedUsd: 350, }, isAssignable: false, subsidyUuid: 'mock-subsidy-uuid', }; + +export const mockSubsidySummary = { + offerId: '84014', + budgets: [], + enterpriseCustomerUuid: '852eac48-b5a9-4849-8490-743f3f2deabf', + enterpriseName: 'Executive Education (2U) Integration QA', + sumAmountLearnerPaid: 0.0, + sumCoursePrice: 0.0, + status: 'Open', + offerType: 'Site', + dateCreated: '2022-09-23T12:37:32Z', + emailsForUsageAlert: '', + discountType: 'percent_discount', + discountValue: 100.0, + maxDiscount: 50000.0, + totalDiscountEcommerce: 42024.0, + amountOfOfferSpent: 0.0, + percentOfOfferSpent: 0.0, + remainingBalance: 50000.0, + amountOfferSpentOcm: 0.0, + amountOfferSpentExecEd: 0.0, + exportTimestamp: '2023-12-04T06:47:54Z', +}; + +export const mockEnterpriseOfferMetadata = { + id: 99511, + startDatetime: '2022-09-01T00:00:00Z', + endDatetime: '2024-09-01T00:00:00Z', + displayName: 'Test enterprise', +}; diff --git a/src/components/learner-credit-management/search/CatalogSearch.jsx b/src/components/learner-credit-management/search/CatalogSearch.jsx index a557a66dc3..f7174d2cc4 100644 --- a/src/components/learner-credit-management/search/CatalogSearch.jsx +++ b/src/components/learner-credit-management/search/CatalogSearch.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import algoliasearch from 'algoliasearch/lite'; import { Configure, InstantSearch } from 'react-instantsearch-dom'; @@ -19,6 +18,7 @@ const CatalogSearch = () => { } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const searchFilters = `enterprise_catalog_uuids:${ENABLE_TESTING(subsidyAccessPolicy.catalogUuid)} AND content_type:course`; const displayName = subsidyAccessPolicy.displayName ? `${subsidyAccessPolicy.displayName} catalog` : 'Overview'; + return (

    ({ useBudgetRedemptions: jest.fn(), useBudgetContentAssignments: jest.fn(), useSubsidyAccessPolicy: jest.fn(), + useSubsidySummaryAnalyticsApi: jest.fn(), + useEnterpriseOffer: jest.fn(), useBudgetDetailActivityOverview: jest.fn(), useIsLargeOrGreater: jest.fn().mockReturnValue(true), useCancelContentAssignments: jest.fn(), @@ -183,6 +192,16 @@ const BudgetDetailPageWrapper = ({ describe('', () => { beforeEach(() => { jest.resetAllMocks(); + + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: {}, + }); + + useEnterpriseOffer.mockReturnValue({ + isLoading: false, + data: {}, + }); }); it('renders page not found messaging if budget is a subsidy access policy, but the REST API returns a 404', () => { @@ -219,6 +238,7 @@ describe('', () => { isLoading: false, data: mockEmptyStateBudgetDetailActivityOverview, }); + const expectedDisplayName = displayName || 'Overview'; renderWithRouter(); @@ -230,6 +250,180 @@ describe('', () => { expect(screen.getByText(expectedDisplayName, { selector: 'h2' })); }); + it.each([ + { + subsidyAccessPolicy: null, + subsidySummary: null, + expected: null, + isLoading: true, + }, + { + subsidyAccessPolicy: null, + subsidySummary: null, + expected: null, + isLoading: false, + }, + { + subsidyAccessPolicy: mockAssignableSubsidyAccessPolicy, + subsidySummary: null, + expected: { + displayName: mockAssignableSubsidyAccessPolicy.displayName, + spend: formatPrice(mockAssignableSubsidyAccessPolicy.aggregates.spendAvailableUsd), + utilized: formatPrice( + mockAssignableSubsidyAccessPolicy.aggregates.amountAllocatedUsd + + mockAssignableSubsidyAccessPolicy.aggregates.amountRedeemedUsd, + ), + limit: formatPrice(mockAssignableSubsidyAccessPolicy.spendLimit / 100), + allocated: formatPrice(mockAssignableSubsidyAccessPolicy.aggregates.amountAllocatedUsd), + redeemed: formatPrice(mockAssignableSubsidyAccessPolicy.aggregates.amountRedeemedUsd), + }, + isLoading: false, + }, + { + subsidyAccessPolicy: mockAssignableSubsidyAccessPolicyWithNoUtilization, + subsidySummary: null, + expected: { + displayName: mockAssignableSubsidyAccessPolicyWithNoUtilization.displayName, + spend: formatPrice(mockAssignableSubsidyAccessPolicyWithNoUtilization.aggregates.spendAvailableUsd), + utilized: formatPrice( + mockAssignableSubsidyAccessPolicyWithNoUtilization.aggregates.amountAllocatedUsd + + mockAssignableSubsidyAccessPolicyWithNoUtilization.aggregates.amountRedeemedUsd, + ), + limit: formatPrice(mockAssignableSubsidyAccessPolicyWithNoUtilization.spendLimit / 100), + allocated: formatPrice(mockAssignableSubsidyAccessPolicyWithNoUtilization.aggregates.amountAllocatedUsd), + redeemed: formatPrice(mockAssignableSubsidyAccessPolicyWithNoUtilization.aggregates.amountRedeemedUsd), + }, + isLoading: false, + }, + { + subsidyAccessPolicy: mockAssignableSubsidyAccessPolicyWithSpendNoAllocations, + subsidySummary: null, + expected: { + displayName: mockAssignableSubsidyAccessPolicyWithSpendNoAllocations.displayName, + spend: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoAllocations.aggregates.spendAvailableUsd), + utilized: formatPrice( + mockAssignableSubsidyAccessPolicyWithSpendNoAllocations.aggregates.amountAllocatedUsd + + mockAssignableSubsidyAccessPolicyWithSpendNoAllocations.aggregates.amountRedeemedUsd, + ), + limit: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoAllocations.spendLimit / 100), + allocated: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoAllocations.aggregates.amountAllocatedUsd), + redeemed: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoAllocations.aggregates.amountRedeemedUsd), + }, + isLoading: false, + }, + { + subsidyAccessPolicy: mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed, + subsidySummary: null, + expected: { + displayName: mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed.displayName, + spend: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed.aggregates.spendAvailableUsd), + utilized: formatPrice( + mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed.aggregates.amountAllocatedUsd + + mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed.aggregates.amountRedeemedUsd, + ), + limit: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed.spendLimit / 100), + allocated: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed.aggregates.amountAllocatedUsd), + redeemed: formatPrice(mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed.aggregates.amountRedeemedUsd), + }, + isLoading: false, + }, + { + subsidyAccessPolicy: mockPerLearnerSpendLimitSubsidyAccessPolicy, + subsidySummary: null, + expected: { + displayName: mockPerLearnerSpendLimitSubsidyAccessPolicy.displayName, + spend: formatPrice(mockPerLearnerSpendLimitSubsidyAccessPolicy.aggregates.spendAvailableUsd), + utilized: formatPrice(mockPerLearnerSpendLimitSubsidyAccessPolicy.aggregates.amountRedeemedUsd), + limit: formatPrice(mockPerLearnerSpendLimitSubsidyAccessPolicy.spendLimit / 100), + allocated: formatPrice(mockPerLearnerSpendLimitSubsidyAccessPolicy.aggregates.amountAllocatedUsd), + redeemed: formatPrice(mockPerLearnerSpendLimitSubsidyAccessPolicy.aggregates.amountRedeemedUsd), + }, + isLoading: false, + }, + { + subsidyAccessPolicy: null, + subsidySummary: mockSubsidySummary, + expected: { + displayName: mockEnterpriseOfferMetadata.displayName, + spend: formatPrice(mockSubsidySummary.remainingBalance), + utilized: formatPrice(mockSubsidySummary.amountOfOfferSpent), + limit: formatPrice(mockSubsidySummary.maxDiscount), + allocated: formatPrice(0), + redeemed: formatPrice(mockSubsidySummary.amountOfOfferSpent), + }, + isLoading: false, + }, + ])('render budget banner data (%s)', async ({ + subsidyAccessPolicy, subsidySummary, expected, isLoading, + }) => { + useParams.mockReturnValue({ + budgetId: 'a52e6548-649f-4576-b73f-c5c2bee25e9c', + activeTabKey: 'activity', + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + isLoading, + data: subsidyAccessPolicy, + }); + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading, + subsidySummary, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: undefined, + spentTransactions: { count: 0 }, + }, + }); + useEnterpriseOffer.mockReturnValue({ + isLoading: false, + data: mockEnterpriseOfferMetadata, + }); + useBudgetRedemptions.mockReturnValue({ + isLoading: false, + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), + }); + + renderWithRouter(); + + if (isLoading) { + expect(screen.getByTestId('budget-detail-skeleton')); + } + + if (subsidyAccessPolicy?.isAssignable) { + const redeemed = subsidyAccessPolicy.aggregates.amountRedeemedUsd; + const allocated = subsidyAccessPolicy.aggregates.amountAllocatedUsd; + + const utilized = redeemed + allocated; + + if (utilized > 0) { + userEvent.click(screen.getByText('Utilization details')); + + expect(screen.getByTestId('budget-utilization-amount')).toHaveTextContent(expected.utilized); + expect(screen.getByTestId('budget-utilization-assigned')).toHaveTextContent(expected.allocated); + expect(screen.getByTestId('budget-utilization-spent')).toHaveTextContent(expected.redeemed); + + if (allocated <= 0) { + expect(screen.queryByText('View assigned activity')).not.toBeInTheDocument(); + } + + if (redeemed <= 0) { + expect(screen.queryByText('View spent activity')).not.toBeInTheDocument(); + } + } + } + + if ((subsidySummary || subsidySummary) && !isLoading) { + expect(screen.getByText(expected.displayName, { selector: 'h2' })); + + expect(screen.getByTestId('budget-detail-available')).toHaveTextContent(expected.spend); + expect(screen.getByTestId('budget-detail-utilized')).toHaveTextContent(`Utilized ${expected.utilized}`); + expect(screen.getByTestId('budget-detail-limit')).toHaveTextContent(expected.limit); + } + }); + it.each([ { isLargeViewport: true }, { isLargeViewport: false }, diff --git a/src/data/services/EcommerceApiService.js b/src/data/services/EcommerceApiService.js index feeed25000..d599f7649c 100644 --- a/src/data/services/EcommerceApiService.js +++ b/src/data/services/EcommerceApiService.js @@ -88,6 +88,16 @@ class EcommerceApiService { return EcommerceApiService.apiClient().post(url, options); } + static fetchEnterpriseOffer(budgetId, options) { + const { enterpriseId } = store.getState().portalConfiguration; + let url = `${EcommerceApiService.ecommerceBaseUrl}/api/v2/enterprise/${enterpriseId}/enterprise-admin-offers/${budgetId}/`; + if (options) { + const queryParams = new URLSearchParams(snakeCaseObject(options)); + url += `?${queryParams.toString()}`; + } + return EcommerceApiService.apiClient().get(url); + } + static fetchEnterpriseOffers(options) { const { enterpriseId } = store.getState().portalConfiguration; let url = `${EcommerceApiService.ecommerceBaseUrl}/api/v2/enterprise/${enterpriseId}/enterprise-admin-offers/`; From 5aa596cfdbc126a22508fa5c17c4a6c79b327404 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 8 Dec 2023 16:56:16 -0500 Subject: [PATCH 107/124] fix: use correct aggregates properties and resolve JS error (#1130) --- .env.development-stage | 1 + .../BudgetDetailPage.jsx | 17 ++++- .../BudgetDetailPageHeader.jsx | 25 ++++--- .../BudgetDetailPageOverviewAvailability.jsx | 38 ++++++---- .../BudgetDetailPageOverviewUtilization.jsx | 49 ++++++------ .../BudgetDetailPageWrapper.jsx | 28 +++++-- .../BudgetDetailRedemptions.jsx | 9 ++- .../data/hooks/useBudgetDetailHeaderData.js | 74 +++++++++++-------- .../data/hooks/useBudgetRedemptions.js | 4 +- .../data/tests/constants.js | 27 ++----- .../tests/BudgetDetailPage.test.jsx | 20 +++-- 11 files changed, 173 insertions(+), 119 deletions(-) diff --git a/.env.development-stage b/.env.development-stage index 39accee150..9fda466b1b 100644 --- a/.env.development-stage +++ b/.env.development-stage @@ -70,6 +70,7 @@ ACCOUNT_PROFILE_URL="https://profile.stage.edx.org" SUPPORT_URL="https://support.edx.org" ENTERPRISE_SUPPORT_URL="https://business-support.edx.org/hc/en-us" ENTERPRISE_SUPPORT_PROGRAM_OPTIMIZATION_URL="https://business.edx.org/hubfs/Onboarding%20and%20Engagement/Onboarding%20Assets/Admin%20Resources/Program%20Optimization.pdf?hsLang=en" +ENTERPRISE_SUPPORT_LEARNER_CREDIT_URL='http://stage.edx.org' CONTACT_URL="https://courses.stage.edx.org/support/contact_us" OPEN_SOURCE_URL="https://open.edx.org" TERMS_OF_SERVICE_URL="https://stage.edx.org/edx-terms-service" diff --git a/src/components/learner-credit-management/BudgetDetailPage.jsx b/src/components/learner-credit-management/BudgetDetailPage.jsx index 3a7192e5e4..49b0feda86 100644 --- a/src/components/learner-credit-management/BudgetDetailPage.jsx +++ b/src/components/learner-credit-management/BudgetDetailPage.jsx @@ -1,22 +1,28 @@ import React from 'react'; import { Skeleton, Stack } from '@edx/paragon'; -import { useBudgetId, useSubsidyAccessPolicy } from './data'; +import { useBudgetId, useEnterpriseOffer, useSubsidyAccessPolicy } from './data'; import BudgetDetailTabsAndRoutes from './BudgetDetailTabsAndRoutes'; import BudgetDetailPageWrapper from './BudgetDetailPageWrapper'; import BudgetDetailPageHeader from './BudgetDetailPageHeader'; import NotFoundPage from '../NotFoundPage'; const BudgetDetailPage = () => { - const { subsidyAccessPolicyId } = useBudgetId(); + const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy, isInitialLoading: isSubsidyAccessPolicyInitialLoading, isError: isSubsidyAccessPolicyError, error, } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + data: enterpriseOffer, + isInitialLoading: isEnterpriseOfferInitialLoading, + } = useEnterpriseOffer(enterpriseOfferId); + + const isLoading = isSubsidyAccessPolicyInitialLoading || isEnterpriseOfferInitialLoading; - if (isSubsidyAccessPolicyInitialLoading) { + if (isLoading) { return ( @@ -35,7 +41,10 @@ const BudgetDetailPage = () => { } return ( - + diff --git a/src/components/learner-credit-management/BudgetDetailPageHeader.jsx b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx index 2ea4b409e1..6269ba44fa 100644 --- a/src/components/learner-credit-management/BudgetDetailPageHeader.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx @@ -28,14 +28,7 @@ const BudgetStatusBadge = ({ ); -BudgetStatusBadge.propTypes = { - badgeVariant: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - term: PropTypes.string.isRequired, - date: PropTypes.string.isRequired, -}; - -const BudgetDetailPageHeader = ({ enterpriseUUID }) => { +const BudgetDetailPageHeader = ({ enterpriseUUID, enterpriseFeatures }) => { const { subsidyAccessPolicyId, enterpriseOfferId } = useBudgetId(); const budgetType = (enterpriseOfferId !== null) ? BUDGET_TYPES.ecommerce : BUDGET_TYPES.policy; @@ -64,6 +57,7 @@ const BudgetDetailPageHeader = ({ enterpriseUUID }) => { subsidySummary, budgetId: policyOrOfferId, enterpriseOfferMetadata, + isTopDownAssignmentEnabled: enterpriseFeatures.topDownAssignmentRealTimeLcm, }); if (!subsidyAccessPolicy && (isLoadingSubsidySummary || isLoadingEnterpriseOffer)) { @@ -75,10 +69,6 @@ const BudgetDetailPageHeader = ({ enterpriseUUID }) => { ); } - if (subsidyAccessPolicy === null && subsidySummary === null) { - return null; - } - return ( @@ -105,10 +95,21 @@ const BudgetDetailPageHeader = ({ enterpriseUUID }) => { const mapStateToProps = state => ({ enterpriseUUID: state.portalConfiguration.enterpriseId, + enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, }); BudgetDetailPageHeader.propTypes = { enterpriseUUID: PropTypes.string.isRequired, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool, + }).isRequired, +}; + +BudgetStatusBadge.propTypes = { + badgeVariant: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + term: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, }; export default connect(mapStateToProps)(BudgetDetailPageHeader); diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index 761fa1085d..78d32ef5f4 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { connect } from 'react-redux'; import { - Button, Col, Hyperlink, ProgressBar, Row, Stack, + Button, Col, Hyperlink, ProgressBar, Row, Stack, useMediaQuery, breakpoints, } from '@edx/paragon'; import { Add } from '@edx/paragon/icons'; import { generatePath, useRouteMatch, Link } from 'react-router-dom'; @@ -40,13 +42,15 @@ const BudgetActions = ({ budgetId, isAssignable }) => { const routeMatch = useRouteMatch(); const supportUrl = configuration.ENTERPRISE_SUPPORT_URL; + const isLargeScreenOrGreater = useMediaQuery({ query: `(min-width: ${breakpoints.small.minWidth}px)` }); + if (!isAssignable) { return ( -
    +

    Get people learning using this budget

    - Funds from this budget are set to autoallocate to registered learners based on + Funds from this budget are set to auto-allocate to registered learners based on settings configured with your support team.

    @@ -62,10 +64,13 @@ const SubBudgetCard = ({ title={{budgetType}} subtitle={{subtitle}} actions={ - budgetLabel.status !== BUDGET_STATUSES.scheduled - ? renderActions(budgetId) - : undefined - } + budgetLabel.status !== BUDGET_STATUSES.scheduled + ? renderActions(budgetId) + : undefined + } + className={classNames('align-items-center', { + 'mb-4.5': budgetLabel.status !== BUDGET_STATUSES.active, + })} /> ); }; @@ -106,9 +111,9 @@ const SubBudgetCard = ({ isLoading={isLoading} > - + {renderCardHeader(displayName || 'Overview', id)} - {budgetLabel.status !== BUDGET_STATUSES.scheduled && renderCardSection()} + {budgetLabel.status === BUDGET_STATUSES.active && renderCardSection()} diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index b030cb6d17..4594de1b00 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -76,7 +76,140 @@ describe('', () => { jest.clearAllMocks(); }); - it('displays correctly for Enterprise Offers (ecommerce)', () => { + it('displays correctly for a scheduled Enterprise Offers (ecommerce)', () => { + const mockBudget = { + id: mockEnterpriseOfferId, + name: mockBudgetDisplayName, + start: '3022-01-01', + end: '3023-01-01', + source: BUDGET_TYPES.ecommerce, + }; + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: { + totalFunds: mockBudgetAggregates.total, + redeemedFunds: mockBudgetAggregates.spent, + remainingFunds: mockBudgetAggregates.available, + percentUtilized: mockBudgetAggregates.spent / mockBudgetAggregates.total, + offerType: 'Site', + offerId: mockEnterpriseOfferId, + budgetsSummary: [], + }, + }); + + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Starts ${dayjs(mockBudget.start).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + }); + + it('displays correctly for a scheduled Subsidy (enterprise-subsidy)', () => { + const mockBudget = { + id: mockEnterpriseOfferId, + name: mockBudgetDisplayName, + start: '3022-01-01', + end: '4023-01-01', + source: BUDGET_TYPES.subsidy, + }; + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: { + totalFunds: mockBudgetAggregates.total, + redeemedFunds: mockBudgetAggregates.spent, + remainingFunds: mockBudgetAggregates.available, + percentUtilized: mockBudgetAggregates.spent / mockBudgetAggregates.total, + offerType: 'Site', + offerId: mockEnterpriseOfferId, + budgetsSummary: [ + { + id: 'test-subsidy-uuid', + start: '3022-01-01', + end: '4023-01-01', + remainingFunds: mockBudgetAggregates.available, + redeemedFunds: mockBudgetAggregates.spent, + enterpriseSlug, + subsidyAccessPolicyDisplayName: mockBudgetDisplayName, + subsidyAccessPolicyUuid: mockBudgetUuid, + }, + ], + }, + }); + + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Starts ${dayjs(mockBudget.start).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + }); + + it.each([ + { isAssignableBudget: false }, + { isAssignableBudget: true }, + ])('displays correctly for a scheduled Policy (enterprise-access) (%s)', ({ isAssignableBudget }) => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + pending: 100, + available: isAssignableBudget ? 4700 : 4800, + }; + const mockBudget = { + id: mockBudgetUuid, + name: mockBudgetDisplayName, + start: '3022-01-01', + end: '4023-01-01', + source: BUDGET_TYPES.policy, + aggregates: { + available: mockBudgetAggregates.available, + pending: isAssignableBudget ? mockBudgetAggregates.pending : undefined, + spent: mockBudgetAggregates.spent, + }, + isAssignable: isAssignableBudget, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: undefined, + }); + + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Starts ${dayjs(mockBudget.start).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + }); + + it('displays correctly for an expired Enterprise Offers (ecommerce)', () => { const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, @@ -119,6 +252,154 @@ describe('', () => { const viewBudgetCTA = screen.getByText('View budget', { selector: 'a' }); expect(viewBudgetCTA).toBeInTheDocument(); expect(viewBudgetCTA).toHaveAttribute('href', `/${enterpriseSlug}/admin/learner-credit/${mockEnterpriseOfferId}`); + }); + + it('displays correctly for an expired Subsidy (enterprise-subsidy)', () => { + const mockBudget = { + id: mockEnterpriseOfferId, + name: mockBudgetDisplayName, + start: '2022-01-01', + end: '2023-01-01', + source: BUDGET_TYPES.subsidy, + }; + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: { + totalFunds: mockBudgetAggregates.total, + redeemedFunds: mockBudgetAggregates.spent, + remainingFunds: mockBudgetAggregates.available, + percentUtilized: mockBudgetAggregates.spent / mockBudgetAggregates.total, + offerType: 'Site', + offerId: mockEnterpriseOfferId, + budgetsSummary: [ + { + id: 'test-subsidy-uuid', + start: '2022-01-01', + end: '2022-01-01', + remainingFunds: mockBudgetAggregates.available, + redeemedFunds: mockBudgetAggregates.spent, + enterpriseSlug, + subsidyAccessPolicyDisplayName: mockBudgetDisplayName, + subsidyAccessPolicyUuid: mockBudgetUuid, + }, + ], + }, + }); + + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Expired ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + + // View budget CTA + const viewBudgetCTA = screen.getByText('View budget', { selector: 'a' }); + expect(viewBudgetCTA).toBeInTheDocument(); + expect(viewBudgetCTA).toHaveAttribute('href', `/${enterpriseSlug}/admin/learner-credit/${mockBudgetUuid}`); + }); + + it.each([ + { isAssignableBudget: false }, + { isAssignableBudget: true }, + ])('displays correctly for an expired Policy (enterprise-access) (%s)', ({ isAssignableBudget }) => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + pending: 100, + available: isAssignableBudget ? 4700 : 4800, + }; + const mockBudget = { + id: mockBudgetUuid, + name: mockBudgetDisplayName, + start: '2022-01-01', + end: '2023-01-01', + source: BUDGET_TYPES.policy, + aggregates: { + available: mockBudgetAggregates.available, + pending: isAssignableBudget ? mockBudgetAggregates.pending : undefined, + spent: mockBudgetAggregates.spent, + }, + isAssignable: isAssignableBudget, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: undefined, + }); + + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Expired ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + + // View budget CTA + const viewBudgetCTA = screen.getByText('View budget', { selector: 'a' }); + expect(viewBudgetCTA).toBeInTheDocument(); + expect(viewBudgetCTA).toHaveAttribute('href', `/${enterpriseSlug}/admin/learner-credit/${mockBudgetUuid}`); + }); + + it('displays correctly for a current Enterprise Offers (ecommerce)', () => { + const mockBudget = { + id: mockEnterpriseOfferId, + name: mockBudgetDisplayName, + start: '2022-01-01', + end: '3022-01-01', + source: BUDGET_TYPES.ecommerce, + }; + const mockBudgetAggregates = { + total: 5000, + spent: 200, + available: 4800, + }; + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: { + totalFunds: mockBudgetAggregates.total, + redeemedFunds: mockBudgetAggregates.spent, + remainingFunds: mockBudgetAggregates.available, + percentUtilized: mockBudgetAggregates.spent / mockBudgetAggregates.total, + offerType: 'Site', + offerId: mockEnterpriseOfferId, + budgetsSummary: [], + }, + }); + + render(); + + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + const formattedString = `Expires ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + + // View budget CTA + const viewBudgetCTA = screen.getByText('View budget', { selector: 'a' }); + expect(viewBudgetCTA).toBeInTheDocument(); + expect(viewBudgetCTA).toHaveAttribute('href', `/${enterpriseSlug}/admin/learner-credit/${mockEnterpriseOfferId}`); // Aggregates expect(screen.getByText('Balance')).toBeInTheDocument(); @@ -128,12 +409,12 @@ describe('', () => { expect(screen.getByText(formatPrice(mockBudgetAggregates.spent))).toBeInTheDocument(); }); - it('displays correctly for Subsidy (enterprise-subsidy)', () => { + it('displays correctly for a current Subsidy (enterprise-subsidy)', () => { const mockBudget = { id: mockEnterpriseOfferId, name: mockBudgetDisplayName, start: '2022-01-01', - end: '2023-01-01', + end: '3023-01-01', source: BUDGET_TYPES.subsidy, }; const mockBudgetAggregates = { @@ -154,7 +435,7 @@ describe('', () => { { id: 'test-subsidy-uuid', start: '2022-01-01', - end: '2022-01-01', + end: '3023-01-01', remainingFunds: mockBudgetAggregates.available, redeemedFunds: mockBudgetAggregates.spent, enterpriseSlug, @@ -173,7 +454,7 @@ describe('', () => { expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); - const formattedString = `Expired ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const formattedString = `Expires ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; const elementsWithTestId = screen.getAllByTestId('budget-date'); const firstElementWithTestId = elementsWithTestId[0]; expect(firstElementWithTestId).toHaveTextContent(formattedString); @@ -194,7 +475,7 @@ describe('', () => { it.each([ { isAssignableBudget: false }, { isAssignableBudget: true }, - ])('displays correctly for Policy (enterprise-access) (%s)', ({ isAssignableBudget }) => { + ])('displays correctly for a current Policy (enterprise-access) (%s)', ({ isAssignableBudget }) => { const mockBudgetAggregates = { total: 5000, spent: 200, @@ -205,7 +486,7 @@ describe('', () => { id: mockBudgetUuid, name: mockBudgetDisplayName, start: '2022-01-01', - end: '2023-01-01', + end: '3023-01-01', source: BUDGET_TYPES.policy, aggregates: { available: mockBudgetAggregates.available, @@ -227,7 +508,7 @@ describe('', () => { expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); - const formattedString = `Expired ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; + const formattedString = `Expires ${dayjs(mockBudget.end).format('MMMM D, YYYY')}`; const elementsWithTestId = screen.getAllByTestId('budget-date'); const firstElementWithTestId = elementsWithTestId[0]; expect(firstElementWithTestId).toHaveTextContent(formattedString); From 9d98bdd2db48ccdcf43f4f7ad8c62899550936c5 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Mon, 11 Dec 2023 13:53:44 -0500 Subject: [PATCH 109/124] fix: replace pending with assigned in budget cards (#1134) --- src/components/learner-credit-management/SubBudgetCard.jsx | 2 +- .../learner-credit-management/tests/BudgetCard.test.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index ff81c89f0c..6261d3762a 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -89,7 +89,7 @@ const SubBudgetCard = ({ {isAssignable && ( -
    Pending
    +
    Assigned
    {isFetchingBudgets ? : formatPrice(pending)} diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 4594de1b00..f146dc3111 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -523,10 +523,10 @@ describe('', () => { expect(screen.getByText('Available')).toBeInTheDocument(); expect(screen.getByText(formatPrice(mockBudgetAggregates.available))).toBeInTheDocument(); if (isAssignableBudget) { - expect(screen.getByText('Pending')).toBeInTheDocument(); + expect(screen.getByText('Assigned')).toBeInTheDocument(); expect(screen.getByText(formatPrice(mockBudgetAggregates.pending))).toBeInTheDocument(); } else { - expect(screen.queryByText('Pending')).not.toBeInTheDocument(); + expect(screen.queryByText('Assigned')).not.toBeInTheDocument(); } expect(screen.getByText('Spent')).toBeInTheDocument(); expect(screen.getByText(formatPrice(mockBudgetAggregates.spent))).toBeInTheDocument(); From 247985ba761d2dccf00753f7cd2588c5f9a2d07c Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Wed, 13 Dec 2023 11:49:42 -0800 Subject: [PATCH 110/124] feat: add assignment status chip for a failed redemption This is the frontend component corresponding to: https://github.com/openedx/enterprise-access/pull/364 Before this change, failed redemptions would result in the frontend displaying a "Failed: System" chip, which is a generic fallback. After this change, an explicit "Failed: Redemption" chip is displayed. ENT-8105 --- .../AssignmentStatusTableCell.jsx | 12 +++- .../FailedRedemption.jsx | 59 +++++++++++++++++++ .../tests/BudgetDetailPage.test.jsx | 23 ++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx diff --git a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx index af24ed54fb..e6c104ab52 100644 --- a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx @@ -4,6 +4,7 @@ import { import PropTypes from 'prop-types'; import FailedBadEmail from './assignments-status-chips/FailedBadEmail'; import FailedCancellation from './assignments-status-chips/FailedCancellation'; +import FailedRedemption from './assignments-status-chips/FailedRedemption'; import FailedReminder from './assignments-status-chips/FailedReminder'; import FailedSystem from './assignments-status-chips/FailedSystem'; import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; @@ -36,17 +37,19 @@ const AssignmentStatusTableCell = ({ row }) => { } if (learnerState === 'failed') { - // If learnerState is failed but no error reason is defined, return a failed system chip. + // If learnerState is failed but no top-level error reason is defined, return a failed system chip. if (!errorReason) { return ; } - // Determine which failure chip to display based on the error reason. + // Determine which failure chip to display based on the top level errorReason. In most cases, the actual errorReason + // code is ignored, in which case we key off the actionType. if (errorReason.actionType === 'notified') { if (errorReason.errorReason === 'email_error') { return ( ); } + // non-email errors on failed notifications should NOT use the FailedBadEmail chip. return ; } if (errorReason.actionType === 'cancelled') { @@ -55,6 +58,11 @@ const AssignmentStatusTableCell = ({ row }) => { if (errorReason.actionType === 'reminded') { return ; } + if (errorReason.actionType === 'redeemed') { + return ; + } + // In all other unexpected cases, return a failed system chip. + return ; } // Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway. diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx new file mode 100644 index 0000000000..6e12f9303f --- /dev/null +++ b/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Error } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; + +import BaseModalPopup from './BaseModalPopup'; + +const FailedRedemption = () => { + const [isOpen, open, close] = useToggle(false); + const [target, setTarget] = useState(null); + + return ( + <> + + Failed: Redemption + + + + Failed: Redemption + + +

    + Something went wrong behind the scenes when the learner attempted to redeem this course assignment. + Associated Learner credit funds have been released into your available balance. +

    +
    +

    Resolution steps

    +
      +
    • + Try assigning this content to the learner again. +
    • +
    • + If the issue continues, contact customer support. +
    • +
    • + Get more troubleshooting help at{' '} + + Help Center: Course Assignments + . +
    • +
    +
    +
    +
    + + ); +}; + +export default FailedRedemption; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 4d4475cd78..afee771b75 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -167,6 +167,12 @@ const mockFailedReminderLearnerAction = { errorReason: 'email_error', }; +const mockFailedRedemptionLearnerAction = { + actionType: 'redeemed', + completedAt: null, + errorReason: 'enrollment_error', +}; + const defaultEnterpriseSubsidiesContextValue = { isLoading: false, }; @@ -1046,6 +1052,8 @@ describe('', () => { actionType: 'notified', }, }, + // This test case is weird because we always serialize the latest failed action into error_reason in the assignment + // API response. Nevertheless, keep it in just to cover potential backend serializer bugs. { learnerState: 'failed', hasLearnerEmail: true, @@ -1091,6 +1099,21 @@ describe('', () => { actionType: 'reminded', }, }, + { + learnerState: 'failed', + hasLearnerEmail: true, + expectedChipStatus: 'Failed: Redemption', + expectedModalPopupHeading: 'Failed: Redemption', + expectedModalPopupContent: ( + 'Something went wrong behind the scenes when the learner attempted to redeem this course assignment. ' + + 'Associated Learner credit funds have been released into your available balance.' + ), + actions: [mockSuccessfulLinkedLearnerAction, mockSuccessfulNotifiedAction, mockFailedRedemptionLearnerAction], + errorReason: { + actionType: mockFailedRedemptionLearnerAction.actionType, + errorReason: mockFailedRedemptionLearnerAction.errorReason, + }, + }, ])('renders correct status chips with assigned table data (%s)', ({ learnerState, hasLearnerEmail, From 4a03e0e7aa84e69453e64b5877743e51d3ee606f Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Wed, 13 Dec 2023 16:44:23 -0500 Subject: [PATCH 111/124] fix: update variant on new assignment button (#1137) --- .../BudgetDetailPageOverviewAvailability.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index 78d32ef5f4..8fca7681f6 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -66,7 +66,7 @@ const BudgetActions = ({ budgetId, isAssignable }) => {

    Get people learning using this budget

    ); @@ -34,10 +52,18 @@ const AssignmentTableCancelAction = ({ selectedFlatRows }) => { AssignmentTableCancelAction.propTypes = { selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), + isEntireTableSelected: PropTypes.bool, + tableInstance: PropTypes.shape({ + itemCount: PropTypes.number.isRequired, + }), }; AssignmentTableCancelAction.defaultProps = { selectedFlatRows: [], + isEntireTableSelected: false, + tableInstance: { + itemCount: 0, + }, }; export default AssignmentTableCancelAction; diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx index e63fbfac72..56f5d7378b 100644 --- a/src/components/learner-credit-management/AssignmentTableRemind.jsx +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -5,33 +5,51 @@ import { Mail } from '@edx/paragon/icons'; import useRemindContentAssignments from './data/hooks/useRemindContentAssignments'; import RemindAssignmentModal from './RemindAssignmentModal'; -const AssignmentTableRemindAction = ({ selectedFlatRows }) => { +const calculateTotalToRemind = ({ + assignmentUuids, + isEntireTableSelected, + learnerStateCounts, +}) => { + if (isEntireTableSelected) { + const waitingAssignmentCounts = learnerStateCounts.filter(({ learnerState }) => (learnerState === 'waiting')); + return waitingAssignmentCounts.length ? waitingAssignmentCounts[0].count : 0; + } + return assignmentUuids.length; +}; + +const AssignmentTableRemindAction = ({ selectedFlatRows, isEntireTableSelected, learnerStateCounts }) => { const assignmentUuids = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').map(({ id }) => id); const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; - const selectedRemindableRows = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').length; const { remindButtonState, remindContentAssignments, close, isOpen, open, - } = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids); + } = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids, isEntireTableSelected); + + const selectedRemindableRowCount = calculateTotalToRemind({ + assignmentUuids, + isEntireTableSelected, + learnerStateCounts, + }); + return ( <> ); @@ -39,10 +57,16 @@ const AssignmentTableRemindAction = ({ selectedFlatRows }) => { AssignmentTableRemindAction.propTypes = { selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), + isEntireTableSelected: PropTypes.bool, + learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({ + learnerState: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + })).isRequired, }; AssignmentTableRemindAction.defaultProps = { selectedFlatRows: [], + isEntireTableSelected: false, }; export default AssignmentTableRemindAction; diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index 9488d2f754..0ece5dc2a2 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -117,7 +117,7 @@ const BudgetAssignmentsTable = ({ pageCount={tableData.numPages || 1} EmptyTableComponent={CustomDataTableEmptyState} bulkActions={[ - , + , , ]} /> diff --git a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js index 189462a776..a21dc68c64 100644 --- a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js +++ b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js @@ -10,6 +10,7 @@ import useBudgetId from './useBudgetId'; const useCancelContentAssignments = ( assignmentConfigurationUuid, assignmentUuids, + cancelAll = false, ) => { const [isOpen, open, close] = useToggle(false); const [cancelButtonState, setCancelButtonState] = useState('default'); @@ -19,7 +20,11 @@ const useCancelContentAssignments = ( const cancelContentAssignments = useCallback(async () => { setCancelButtonState('pending'); try { - await EnterpriseAccessApiService.cancelContentAssignments(assignmentConfigurationUuid, assignmentUuids); + if (cancelAll) { + await EnterpriseAccessApiService.cancelAllContentAssignments(assignmentConfigurationUuid); + } else { + await EnterpriseAccessApiService.cancelContentAssignments(assignmentConfigurationUuid, assignmentUuids); + } setCancelButtonState('complete'); queryClient.invalidateQueries({ queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), @@ -28,7 +33,7 @@ const useCancelContentAssignments = ( logError(err); setCancelButtonState('error'); } - }, [assignmentConfigurationUuid, assignmentUuids, queryClient, subsidyAccessPolicyId]); + }, [assignmentConfigurationUuid, assignmentUuids, cancelAll, queryClient, subsidyAccessPolicyId]); return { cancelButtonState, diff --git a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js index bc55743a30..aaeac80c36 100644 --- a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js +++ b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js @@ -10,6 +10,7 @@ import useBudgetId from './useBudgetId'; const useRemindContentAssignments = ( assignmentConfigurationUuid, assignmentUuids, + remindAll = false, ) => { const [isOpen, open, close] = useToggle(false); const [remindButtonState, setRemindButtonState] = useState('default'); @@ -19,7 +20,11 @@ const useRemindContentAssignments = ( const remindContentAssignments = useCallback(async () => { setRemindButtonState('pending'); try { - await EnterpriseAccessApiService.remindContentAssignments(assignmentConfigurationUuid, assignmentUuids); + if (remindAll) { + await EnterpriseAccessApiService.remindAllContentAssignments(assignmentConfigurationUuid); + } else { + await EnterpriseAccessApiService.remindContentAssignments(assignmentConfigurationUuid, assignmentUuids); + } setRemindButtonState('complete'); queryClient.invalidateQueries({ queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId), @@ -28,7 +33,7 @@ const useRemindContentAssignments = ( logError(err); setRemindButtonState('error'); } - }, [assignmentConfigurationUuid, assignmentUuids, queryClient, subsidyAccessPolicyId]); + }, [assignmentConfigurationUuid, assignmentUuids, remindAll, queryClient, subsidyAccessPolicyId]); return { remindButtonState, diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index afee771b75..9052c8d93c 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -1453,7 +1453,7 @@ describe('', () => { }); it('cancels assignments in bulk', async () => { - EnterpriseAccessApiService.cancelContentAssignments.mockResolvedValueOnce({ status: 200 }); + EnterpriseAccessApiService.cancelAllContentAssignments.mockResolvedValueOnce({ status: 200 }); useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', @@ -1520,16 +1520,18 @@ describe('', () => { expect(modalDialog).toBeInTheDocument(); const cancelDialogButton = getButtonElement('Cancel assignments (2)'); userEvent.click(cancelDialogButton); - expect( - EnterpriseAccessApiService.cancelContentAssignments, - ).toHaveBeenCalled(); + await waitFor( + () => expect( + EnterpriseAccessApiService.cancelAllContentAssignments, + ).toHaveBeenCalled(), + ); await waitFor( () => expect(screen.getByText('Assignments canceled (2)')).toBeInTheDocument(), ); }); it('reminds assignments in bulk', async () => { - EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 }); + EnterpriseAccessApiService.remindAllContentAssignments.mockResolvedValueOnce({ status: 202 }); useParams.mockReturnValue({ budgetId: mockSubsidyAccessPolicyUUID, activeTabKey: 'activity', @@ -1553,7 +1555,7 @@ describe('', () => { useBudgetContentAssignments.mockReturnValue({ isLoading: false, contentAssignments: { - count: 2, + count: 3, results: [ { uuid: 'test-uuid1', @@ -1575,10 +1577,20 @@ describe('', () => { errorReason: null, state: 'allocated', }, + { + uuid: 'test-uuid3', + contentKey: mockCourseKey, + contentQuantity: -29900, + learnerState: 'notifying', + recentAction: { actionType: 'assigned', timestamp: '2023-11-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + }, ], learnerStateCounts: [ - { learnerState: 'waiting', count: 1 }, - { learnerState: 'waiting', count: 1 }, + { learnerState: 'waiting', count: 2 }, + { learnerState: 'notifying', count: 1 }, ], numPages: 1, currentPage: 1, @@ -1596,9 +1608,11 @@ describe('', () => { expect(modalDialog).toBeInTheDocument(); const remindDialogButton = getButtonElement('Send reminders (2)'); userEvent.click(remindDialogButton); - expect( - EnterpriseAccessApiService.remindContentAssignments, - ).toHaveBeenCalled(); + await waitFor( + () => expect( + EnterpriseAccessApiService.remindAllContentAssignments, + ).toHaveBeenCalled(), + ); await waitFor( () => expect(screen.getByText('Reminders sent (2)')).toBeInTheDocument(), ); diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index 8a38cb2b14..7313698db4 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -184,6 +184,14 @@ class EnterpriseAccessApiService { return EnterpriseAccessApiService.apiClient().post(url, options); } + /** + * Cancel ALL content assignments for a specific AssignmentConfiguration. + */ + static cancelAllContentAssignments(assignmentConfigurationUUID) { + const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/cancel-all/`; + return EnterpriseAccessApiService.apiClient().post(url); + } + /** * Remind content assignments for a specific AssignmentConfiguration. */ @@ -195,6 +203,14 @@ class EnterpriseAccessApiService { return EnterpriseAccessApiService.apiClient().post(url, options); } + /** + * Remind ALL content assignments for a specific AssignmentConfiguration. + */ + static remindAllContentAssignments(assignmentConfigurationUUID) { + const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/remind-all/`; + return EnterpriseAccessApiService.apiClient().post(url); + } + /** * Retrieve a specific subsidy access policy. * @param {string} subsidyAccessPolicyUUID The UUID of the subsidy access policy to retrieve. diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index f2007a56f3..864ef63277 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -201,7 +201,7 @@ describe('EnterpriseAccessApiService', () => { ); }); - test('remindContentAssignments calls enterprise-access cancel POST API to remind learners', () => { + test('remindContentAssignments calls enterprise-access remind POST API to remind learners', () => { const options = { assignment_uuids: mockAssignmentUUIDs, }; @@ -211,4 +211,18 @@ describe('EnterpriseAccessApiService', () => { options, ); }); + + test('cancelAllContentAssignments calls enterprise-access cancel-all POST API to cancel all assignments', () => { + EnterpriseAccessApiService.cancelAllContentAssignments(mockAssignmentConfigurationUUID); + expect(axios.post).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/cancel-all/`, + ); + }); + + test('remindAllContentAssignments calls enterprise-access remind-all POST API to remind all learners', () => { + EnterpriseAccessApiService.remindAllContentAssignments(mockAssignmentConfigurationUUID); + expect(axios.post).toBeCalledWith( + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/remind-all/`, + ); + }); }); From 2b52504ff25270cfbd8103b66b5202695b367abc Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 14 Dec 2023 10:43:32 -0500 Subject: [PATCH 113/124] fix: sets button variant to primary (#1139) --- .../BudgetDetailPageOverviewAvailability.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index 8fca7681f6..39fa31dbe8 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -66,7 +66,6 @@ const BudgetActions = ({ budgetId, isAssignable }) => {

    Get people learning using this budget

    { diff --git a/src/components/settings/data/hooks.js b/src/components/settings/data/hooks.js index e907c36a34..cb800dc143 100644 --- a/src/components/settings/data/hooks.js +++ b/src/components/settings/data/hooks.js @@ -132,6 +132,17 @@ export const useStylesForCustomBrandColors = (branding) => { .btn-brand:focus:before { border-color: ${brandColors.secondary.regular.hex()} !important; } + .btn-outline-brand { + color: ${brandColors.secondary.textColor.hex()} !important; + } + .btn-outline-brand:hover { + background-color: ${brandColors.secondary.regular.hex()} !important; + color: ${brandColors.secondary.textColor.hex()} !important; + border-color: ${brandColors.secondary.regular.hex()} !important; + } + .btn-outline-brand:focus:before { + border-color: ${brandColors.secondary.regular.hex()} !important; + } .btn-primary { background-color: ${brandColors.primary.regular.hex()} !important; border-color: ${brandColors.primary.regular.hex()} !important; @@ -141,26 +152,19 @@ export const useStylesForCustomBrandColors = (branding) => { background-color: ${brandColors.primary.dark.hex()} !important; border-color: ${brandColors.primary.dark.hex()} !important; } - .btn-brand-primary { - background-color: ${brandColors.primary.regular.hex()} !important; + .btn-primary:focus:before { border-color: ${brandColors.primary.regular.hex()} !important; - color: ${brandColors.primary.textColor.hex()} !important; } - .btn-brand-primary:hover { - background-color: ${brandColors.primary.dark.hex()} !important; - border-color: ${brandColors.primary.dark.hex()} !important; - } - .btn-brand-primary:focus:before { - border-color: ${brandColors.primary.regular.hex()} !important; + .btn-outline-primary { + color: ${brandColors.primary.textColor.hex()} !important; } - .bg-brand-primary { + .btn-outline-primary:hover { background-color: ${brandColors.primary.regular.hex()} !important; - } - .border-brand-primary { + color: ${brandColors.primary.textColor.hex()} !important; border-color: ${brandColors.primary.regular.hex()} !important; } - .color-brand-tertiary { - color: ${brandColors.tertiary.regular.hex()} !important; + .btn-outline-primary:focus:before { + border-color: ${brandColors.primary.regular.hex()} !important; } .secondary-background { background: ${brandColors.secondary.regular.hex()} !important; diff --git a/src/components/settings/settings.scss b/src/components/settings/settings.scss index 9730540a88..683398b253 100644 --- a/src/components/settings/settings.scss +++ b/src/components/settings/settings.scss @@ -167,10 +167,6 @@ font-weight: 500; } -.stepper-modal .pgn__modal-header { - border-bottom: solid 7px; -} - .warning-icon { color: #F0CC00; } diff --git a/src/index.scss b/src/index.scss index c2bfdfb974..c3a4e794b0 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,13 +1,14 @@ // ... Any custom SCSS variables should be defined here $modal-max-width: 650px; +// Paragon/Brand @import "~@edx/brand/paragon/fonts"; @import "~@edx/brand/paragon/variables"; @import "~@edx/paragon/scss/core/core"; @import "~@edx/frontend-enterprise-catalog-search"; @import "~@edx/brand/paragon/overrides"; - +// Components @import "./components/EnterpriseApp/EnterpriseApp"; @import "./components/CodeSearchResults/CodeSearchResults"; @import "./components/Coupon/Coupon"; @@ -26,6 +27,7 @@ $modal-max-width: 650px; @import "./components/settings/settings"; @import "./components/learner-credit-management/styles"; +// Global body { overflow-x: hidden; } @@ -73,3 +75,7 @@ form { max-width: $modal-max-width; } } + +.stepper-modal .pgn__modal-header { + border-bottom: solid 7px; +} From f2aefb60aa6dfd51a8e5e77ed78b997d7a73cfc2 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Thu, 14 Dec 2023 20:50:30 -0500 Subject: [PATCH 115/124] fix: remove color style on outline button variants for dynamic themes (#1141) --- src/components/settings/data/hooks.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/settings/data/hooks.js b/src/components/settings/data/hooks.js index cb800dc143..5dc6ac6c99 100644 --- a/src/components/settings/data/hooks.js +++ b/src/components/settings/data/hooks.js @@ -132,9 +132,6 @@ export const useStylesForCustomBrandColors = (branding) => { .btn-brand:focus:before { border-color: ${brandColors.secondary.regular.hex()} !important; } - .btn-outline-brand { - color: ${brandColors.secondary.textColor.hex()} !important; - } .btn-outline-brand:hover { background-color: ${brandColors.secondary.regular.hex()} !important; color: ${brandColors.secondary.textColor.hex()} !important; @@ -155,9 +152,6 @@ export const useStylesForCustomBrandColors = (branding) => { .btn-primary:focus:before { border-color: ${brandColors.primary.regular.hex()} !important; } - .btn-outline-primary { - color: ${brandColors.primary.textColor.hex()} !important; - } .btn-outline-primary:hover { background-color: ${brandColors.primary.regular.hex()} !important; color: ${brandColors.primary.textColor.hex()} !important; From 13ad64ca1d5939643201396e180ef67f17bfd44e Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Fri, 15 Dec 2023 09:53:40 -0500 Subject: [PATCH 116/124] fix: account for filters in select all bulk actions assigned table (#1142) --- .../AssignmentTableCancel.jsx | 26 +++++++++--------- .../AssignmentTableRemind.jsx | 27 ++++++++++++------- src/components/subscriptions/data/utils.js | 12 --------- .../bulk-actions/RemindBulkAction.jsx | 5 ++-- .../bulk-actions/RevokeBulkAction.jsx | 5 ++-- src/utils.js | 15 +++++++++++ 6 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/components/learner-credit-management/AssignmentTableCancel.jsx b/src/components/learner-credit-management/AssignmentTableCancel.jsx index 86c5580115..db155be926 100644 --- a/src/components/learner-credit-management/AssignmentTableCancel.jsx +++ b/src/components/learner-credit-management/AssignmentTableCancel.jsx @@ -4,6 +4,7 @@ import { Button } from '@edx/paragon'; import { DoNotDisturbOn } from '@edx/paragon/icons'; import CancelAssignmentModal from './CancelAssignmentModal'; import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; +import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToCancel = ({ assignmentUuids, @@ -19,18 +20,24 @@ const calculateTotalToCancel = ({ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, tableInstance }) => { const assignmentUuids = selectedFlatRows.map(row => row.id); const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; + + const activeFilters = getActiveTableColumnFilters(tableInstance.columns); + + // If entire table is selected and there are NO filters, hit cancel-all endpoint. Otherwise, hit usual bulk cancel. + const shouldCancelAll = isEntireTableSelected && activeFilters.length === 0; + const { cancelButtonState, cancelContentAssignments, close, isOpen, open, - } = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids, isEntireTableSelected); + } = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids, shouldCancelAll); const tableItemCount = tableInstance.itemCount; const totalToCancel = calculateTotalToCancel({ assignmentUuids, - isEntireTableSelected, + isEntireTableSelected: shouldCancelAll, tableItemCount, }); @@ -51,19 +58,12 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, }; AssignmentTableCancelAction.propTypes = { - selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), - isEntireTableSelected: PropTypes.bool, + selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, + isEntireTableSelected: PropTypes.bool.isRequired, tableInstance: PropTypes.shape({ itemCount: PropTypes.number.isRequired, - }), -}; - -AssignmentTableCancelAction.defaultProps = { - selectedFlatRows: [], - isEntireTableSelected: false, - tableInstance: { - itemCount: 0, - }, + columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, + }).isRequired, }; export default AssignmentTableCancelAction; diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx index 56f5d7378b..d501178a11 100644 --- a/src/components/learner-credit-management/AssignmentTableRemind.jsx +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -4,6 +4,7 @@ import { Button } from '@edx/paragon'; import { Mail } from '@edx/paragon/icons'; import useRemindContentAssignments from './data/hooks/useRemindContentAssignments'; import RemindAssignmentModal from './RemindAssignmentModal'; +import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToRemind = ({ assignmentUuids, @@ -17,20 +18,28 @@ const calculateTotalToRemind = ({ return assignmentUuids.length; }; -const AssignmentTableRemindAction = ({ selectedFlatRows, isEntireTableSelected, learnerStateCounts }) => { +const AssignmentTableRemindAction = ({ + selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, +}) => { const assignmentUuids = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').map(({ id }) => id); const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; + + const activeFilters = getActiveTableColumnFilters(tableInstance.columns); + + // If entire table is selected and there are NO filters, hit remind-all endpoint. Otherwise, hit usual bulk remind. + const shouldRemindAll = isEntireTableSelected && activeFilters.length === 0; + const { remindButtonState, remindContentAssignments, close, isOpen, open, - } = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids, isEntireTableSelected); + } = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids, shouldRemindAll); const selectedRemindableRowCount = calculateTotalToRemind({ assignmentUuids, - isEntireTableSelected, + isEntireTableSelected: shouldRemindAll, learnerStateCounts, }); @@ -56,17 +65,15 @@ const AssignmentTableRemindAction = ({ selectedFlatRows, isEntireTableSelected, }; AssignmentTableRemindAction.propTypes = { - selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), - isEntireTableSelected: PropTypes.bool, + selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, + isEntireTableSelected: PropTypes.bool.isRequired, learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({ learnerState: PropTypes.string.isRequired, count: PropTypes.number.isRequired, })).isRequired, -}; - -AssignmentTableRemindAction.defaultProps = { - selectedFlatRows: [], - isEntireTableSelected: false, + tableInstance: PropTypes.shape({ + columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, + }).isRequired, }; export default AssignmentTableRemindAction; diff --git a/src/components/subscriptions/data/utils.js b/src/components/subscriptions/data/utils.js index 2829929b24..77343f53c0 100644 --- a/src/components/subscriptions/data/utils.js +++ b/src/components/subscriptions/data/utils.js @@ -65,15 +65,3 @@ export const transformFiltersForRequest = (filters) => { }), ); }; - -/** - * Helper to determine which table columns have an active filter applied. - * - * @param {object} columns Array of column objects (e.g., { id, filter, filterValue }) - * @returns Array of column objects with an active filter applied. - */ -export const getActiveFilters = columns => columns.map(column => ({ - name: column.id, - filter: column.filter, - filterValue: column.filterValue, -})).filter(filter => !!filter.filterValue); diff --git a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.jsx b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.jsx index b06d85e98b..2d5615c341 100644 --- a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RemindBulkAction.jsx @@ -9,9 +9,10 @@ import { licenseManagementModalZeroState as modalZeroState, } from '../../LicenseManagementModals/LicenseManagementModalHook'; import LicenseManagementRemindModal from '../../LicenseManagementModals/LicenseManagementRemindModal'; -import { canRemindLicense, getActiveFilters } from '../../../data/utils'; +import { canRemindLicense } from '../../../data/utils'; import { ACTIVATED, REVOKED } from '../../../data/constants'; import { SUBSCRIPTION_TABLE_EVENTS } from '../../../../../eventTracking'; +import { getActiveTableColumnFilters } from '../../../../../utils'; const calculateTotalToRemind = ({ selectedRemindableRows, @@ -54,7 +55,7 @@ const RemindBulkAction = ({ const [remindModal, setRemindModal] = useLicenseManagementModalState(); const selectedRows = selectedFlatRows.map(selectedRow => selectedRow.original); const selectedRemindableRows = selectedRows.filter(row => canRemindLicense(row.status)); - const activeFilters = getActiveFilters(tableInstance.columns); + const activeFilters = getActiveTableColumnFilters(tableInstance.columns); const tableItemCount = tableInstance.itemCount; const totalToRemind = calculateTotalToRemind({ selectedRemindableRows, diff --git a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.jsx b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.jsx index 37243dc07f..11726b8344 100644 --- a/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.jsx +++ b/src/components/subscriptions/licenses/LicenseManagementTable/bulk-actions/RevokeBulkAction.jsx @@ -9,9 +9,10 @@ import { licenseManagementModalZeroState as modalZeroState, } from '../../LicenseManagementModals/LicenseManagementModalHook'; import LicenseManagementRevokeModal from '../../LicenseManagementModals/LicenseManagementRevokeModal'; -import { canRevokeLicense, getActiveFilters } from '../../../data/utils'; +import { canRevokeLicense } from '../../../data/utils'; import { REVOKED } from '../../../data/constants'; import { SUBSCRIPTION_TABLE_EVENTS } from '../../../../../eventTracking'; +import { getActiveTableColumnFilters } from '../../../../../utils'; const calculateTotalToRevoke = ({ selectedRevocableRows, @@ -51,7 +52,7 @@ const RevokeBulkAction = ({ const [revokeModal, setRevokeModal] = useLicenseManagementModalState(); const selectedRows = selectedFlatRows.map(selectedRow => selectedRow.original); const selectedRevocableRows = selectedRows.filter(row => canRevokeLicense(row.status)); - const activeFilters = getActiveFilters(tableInstance.columns); + const activeFilters = getActiveTableColumnFilters(tableInstance.columns); const tableItemCount = tableInstance.itemCount; const totalToRevoke = calculateTotalToRevoke({ selectedRevocableRows, diff --git a/src/utils.js b/src/utils.js index f3fa4e103e..b1e2399cb4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -423,6 +423,20 @@ function isAssignableSubsidyAccessPolicyType(policy) { return isAssignable && assignableSubsidyAccessPolicyTypes.includes(policyType); } +/** + * Helper to determine which table columns have an active filter applied. + * + * @param {object} columns Array of column objects (e.g., { id, filter, filterValue }) + * @returns Array of column objects with an active filter applied. + */ +function getActiveTableColumnFilters(columns) { + return columns.map(column => ({ + name: column.id, + filter: column.filter, + filterValue: column.filterValue, + })).filter(filter => !!filter.filterValue); +} + export { camelCaseDict, camelCaseDictArray, @@ -458,4 +472,5 @@ export { isNotValidNumberString, defaultQueryClientRetryHandler, isAssignableSubsidyAccessPolicyType, + getActiveTableColumnFilters, }; From 27a7d6f9118c0b5507f8cbc3dd8079c05017236a Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Fri, 15 Dec 2023 12:52:04 -0500 Subject: [PATCH 117/124] fix: updates package reference for useLocation (#1143) * fix: updates package reference for useLocation * fix: update dependency * chore: PR fixes * chore: Pr fixes 2 --- .../ContentHighlights/tests/ContentHighlights.test.jsx | 2 +- .../BudgetDetailCatalogTabContents.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ContentHighlights/tests/ContentHighlights.test.jsx b/src/components/ContentHighlights/tests/ContentHighlights.test.jsx index 6fc9ca0b38..1561513cb2 100644 --- a/src/components/ContentHighlights/tests/ContentHighlights.test.jsx +++ b/src/components/ContentHighlights/tests/ContentHighlights.test.jsx @@ -5,8 +5,8 @@ import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { renderWithRouter } from '@edx/frontend-enterprise-utils'; -import { useHistory } from 'react-router'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { useHistory } from 'react-router-dom'; import ContentHighlights from '../ContentHighlights'; import { EnterpriseAppContext } from '../../EnterpriseApp/EnterpriseAppContextProvider'; diff --git a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx index af044e0a0f..297dd18eef 100644 --- a/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailCatalogTabContents.jsx @@ -4,7 +4,7 @@ import algoliasearch from 'algoliasearch/lite'; import { Row, Col } from '@edx/paragon'; import { SearchData, SEARCH_FACET_FILTERS } from '@edx/frontend-enterprise-catalog-search'; -import { useHistory } from 'react-router'; +import { useLocation, useHistory } from 'react-router-dom'; import CatalogSearch from './search/CatalogSearch'; import { LANGUAGE_REFINEMENT, @@ -14,7 +14,7 @@ import { configuration } from '../../config'; const BudgetDetailCatalogTabContents = () => { const history = useHistory(); - const { location } = history; + const location = useLocation(); const { state: locationState } = location; const catalogContainerRef = useRef(); From 8f7d90e2831d30bf2d87689644e0d9c566ed36ad Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Tue, 19 Dec 2023 21:12:41 +0000 Subject: [PATCH 118/124] fix: adding country attribute mapping to sso self serve stepper --- .../SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx index 31070334ca..cd2a6e1efe 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx @@ -103,6 +103,14 @@ const SSOConfigConfigureStep = () => { fieldInstructions="URN of SAML attribute containing the user's email address[es]." /> + + + ); const renderSAPFields = () => ( From 2d7e691905069d068295f6743f7aa736c671f40e Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Tue, 2 Jan 2024 09:33:45 -0500 Subject: [PATCH 119/124] feat: add additional segment events (#1132) * feat: add addtional segment events * feat: close and open chip modal events * feat: naming for track events added * feat: more segment eventing work * feat: breadcrumbs and budget overview eventing added * feat: individual cancel and remind modal events * feat: cancel and remind submission events * feat: bulk cancel/remind eventing * feat: Add failed redemption, refator select all events * fix: updates how assignment configuration is retrieved * chore: tests * chore: more tests * chore: PR feedback * feat: add additional metadata across all events * chore: Update tests * chore: PR feedback with abstractions * chore: Update comments * chore: PR fixes * chore: remove extraneous code from test --- .../AssignmentStatusTableCell.jsx | 77 ++++++++++--- .../AssignmentTableCancel.jsx | 101 ++++++++++++++++-- .../AssignmentTableRemind.jsx | 97 +++++++++++++++-- .../BudgetAssignmentsTable.jsx | 2 +- .../BudgetDetailActivityTabContents.jsx | 4 +- .../BudgetDetailPageBreadcrumbs.jsx | 59 +++++++--- .../BudgetDetailPageOverviewAvailability.jsx | 48 ++++++++- .../BudgetDetailPageOverviewUtilization.jsx | 25 ++++- .../CancelAssignmentModal.jsx | 3 + .../PendingAssignmentCancelButton.jsx | 91 +++++++++++++++- .../PendingAssignmentRemindButton.jsx | 90 +++++++++++++++- .../RemindAssignmentModal.jsx | 4 +- .../FailedBadEmail.jsx | 37 +++++-- .../FailedCancellation.jsx | 41 +++++-- .../FailedRedemption.jsx | 43 ++++++-- .../FailedReminder.jsx | 42 ++++++-- .../assignments-status-chips/FailedSystem.jsx | 42 ++++++-- .../NotifyingLearner.jsx | 26 +++-- .../WaitingForLearner.jsx | 38 +++++-- .../cards/NewAssignmentModalButton.jsx | 29 +++-- .../cards/tests/CourseCard.test.jsx | 1 - .../data/hooks/index.js | 1 + .../data/hooks/useAssignmentStatusChip.jsx | 39 +++++++ .../learner-credit-management/data/utils.js | 57 ++++++++++ .../tests/BudgetDetailPage.test.jsx | 25 +++++ .../tests/BudgetDetailPageWrapper.test.jsx | 39 +++++-- src/eventTracking.js | 40 +++++++ 27 files changed, 972 insertions(+), 129 deletions(-) create mode 100644 src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx diff --git a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx index d68cf57d9f..8dc0c0da52 100644 --- a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx @@ -1,5 +1,7 @@ import { Chip } from '@edx/paragon'; import PropTypes from 'prop-types'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import FailedBadEmail from './assignments-status-chips/FailedBadEmail'; import FailedCancellation from './assignments-status-chips/FailedCancellation'; import FailedRedemption from './assignments-status-chips/FailedRedemption'; @@ -8,14 +10,61 @@ import FailedSystem from './assignments-status-chips/FailedSystem'; import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; import WaitingForLearner from './assignments-status-chips/WaitingForLearner'; import { capitalizeFirstLetter } from '../../utils'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; -const AssignmentStatusTableCell = ({ row }) => { +const AssignmentStatusTableCell = ({ enterpriseId, row }) => { const { original } = row; const { learnerEmail, learnerState, errorReason, } = original; + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; + + const sharedTrackEventMetadata = { + learnerState, + subsidyUuid, + assignmentConfiguration, + isSubsidyActive, + isAssignable, + catalogUuid, + aggregates, + }; + + const sendGenericTrackEvent = (eventName, eventMetadata = {}) => { + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + { + ...sharedTrackEventMetadata, + ...eventMetadata, + }, + ); + }; + + const sendErrorStateTrackEvent = (eventName, eventMetadata = {}) => { + const errorReasonMetadata = { + erroredAction: { + errorReason: errorReason?.errorReason || null, + actionType: errorReason?.actionType || null, + }, + }; + const errorStateMetadata = { + ...sharedTrackEventMetadata, + ...errorReasonMetadata, + ...eventMetadata, + }; + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + errorStateMetadata, + ); + }; + // Learner state is not available for this assignment, so don't display anything. if (!learnerState) { return null; @@ -24,43 +73,42 @@ const AssignmentStatusTableCell = ({ row }) => { // Display the appropriate status chip based on the learner state. if (learnerState === 'notifying') { return ( - + ); } if (learnerState === 'waiting') { return ( - + ); } if (learnerState === 'failed') { // If learnerState is failed but no top-level error reason is defined, return a failed system chip. if (!errorReason) { - return ; + return ; } // Determine which failure chip to display based on the top level errorReason. In most cases, the actual errorReason // code is ignored, in which case we key off the actionType. if (errorReason.actionType === 'notified') { if (errorReason.errorReason === 'email_error') { return ( - + ); } - // non-email errors on failed notifications should NOT use the FailedBadEmail chip. - return ; + return ; } if (errorReason.actionType === 'cancelled') { - return ; + return ; } if (errorReason.actionType === 'reminded') { - return ; + return ; } if (errorReason.actionType === 'redeemed') { - return ; + return ; } // In all other unexpected cases, return a failed system chip. - return ; + return ; } // Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway. @@ -68,6 +116,7 @@ const AssignmentStatusTableCell = ({ row }) => { }; AssignmentStatusTableCell.propTypes = { + enterpriseId: PropTypes.string.isRequired, row: PropTypes.shape({ original: PropTypes.shape({ learnerEmail: PropTypes.string, @@ -84,4 +133,8 @@ AssignmentStatusTableCell.propTypes = { }).isRequired, }; -export default AssignmentStatusTableCell; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentStatusTableCell); diff --git a/src/components/learner-credit-management/AssignmentTableCancel.jsx b/src/components/learner-credit-management/AssignmentTableCancel.jsx index db155be926..eac61c8f31 100644 --- a/src/components/learner-credit-management/AssignmentTableCancel.jsx +++ b/src/components/learner-credit-management/AssignmentTableCancel.jsx @@ -2,8 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from '@edx/paragon'; import { DoNotDisturbOn } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import CancelAssignmentModal from './CancelAssignmentModal'; import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; +import { transformSelectedRows, useBudgetId, useSubsidyAccessPolicy } from './data'; +import EVENT_NAMES from '../../eventTracking'; import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToCancel = ({ @@ -17,9 +21,23 @@ const calculateTotalToCancel = ({ return assignmentUuids.length; }; -const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, tableInstance }) => { - const assignmentUuids = selectedFlatRows.map(row => row.id); - const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; +const AssignmentTableCancelAction = ({ + selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, enterpriseId, +}) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; + + const { + uniqueLearnerState, + uniqueAssignmentState, + uniqueContentKeys, + totalContentQuantity, + assignmentUuids, + totalSelectedRows, + } = transformSelectedRows(selectedFlatRows); const activeFilters = getActiveTableColumnFilters(tableInstance.columns); @@ -32,7 +50,66 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, close, isOpen, open, - } = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids, shouldCancelAll); + } = useCancelContentAssignments(assignmentConfiguration.uuid, assignmentUuids, shouldCancelAll); + + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName) => { + // constructs a learner state object for the select all state to match format of select all on page metadata + const learnerStateObject = {}; + learnerStateCounts.forEach((learnerState) => { + learnerStateObject[learnerState.learnerState] = learnerState.count; + }); + + const selectedRowsMetadata = isEntireTableSelected + ? { uniqueLearnerState: learnerStateObject, totalSelectedRows: tableInstance.itemCount } + : { + uniqueLearnerState, uniqueAssignmentState, uniqueContentKeys, totalContentQuantity, totalSelectedRows, + }; + + const trackEventMetadata = { + ...selectedRowsMetadata, + isAssignable, + isSubsidyActive, + subsidyUuid, + catalogUuid, + isEntireTableSelected, + assignmentUuids, + aggregates, + assignmentConfiguration, + isOpen: !isOpen, + }; + + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL, + ); + }; + + const cancellationTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL, + ); + }; const tableItemCount = tableInstance.itemCount; const totalToCancel = calculateTotalToCancel({ @@ -43,14 +120,15 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, return ( <> - @@ -58,12 +136,21 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, }; AssignmentTableCancelAction.propTypes = { + enterpriseId: PropTypes.string.isRequired, selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, isEntireTableSelected: PropTypes.bool.isRequired, + learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({ + learnerState: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + })).isRequired, tableInstance: PropTypes.shape({ itemCount: PropTypes.number.isRequired, columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, }).isRequired, }; -export default AssignmentTableCancelAction; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentTableCancelAction); diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx index d501178a11..d4eb2f00f1 100644 --- a/src/components/learner-credit-management/AssignmentTableRemind.jsx +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -2,8 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from '@edx/paragon'; import { Mail } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import useRemindContentAssignments from './data/hooks/useRemindContentAssignments'; import RemindAssignmentModal from './RemindAssignmentModal'; +import { transformSelectedRows, useBudgetId, useSubsidyAccessPolicy } from './data'; +import EVENT_NAMES from '../../eventTracking'; import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToRemind = ({ @@ -19,10 +23,23 @@ const calculateTotalToRemind = ({ }; const AssignmentTableRemindAction = ({ - selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, + selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, enterpriseId, }) => { - const assignmentUuids = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').map(({ id }) => id); - const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; + + const remindableRows = selectedFlatRows.filter(row => row.original.learnerState === 'waiting'); + const { + uniqueLearnerState, + uniqueAssignmentState, + uniqueContentKeys, + totalContentQuantity, + assignmentUuids, + totalSelectedRows, + } = transformSelectedRows(remindableRows); const activeFilters = getActiveTableColumnFilters(tableInstance.columns); @@ -35,7 +52,7 @@ const AssignmentTableRemindAction = ({ close, isOpen, open, - } = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids, shouldRemindAll); + } = useRemindContentAssignments(assignmentConfiguration.uuid, assignmentUuids, shouldRemindAll); const selectedRemindableRowCount = calculateTotalToRemind({ assignmentUuids, @@ -43,21 +60,81 @@ const AssignmentTableRemindAction = ({ learnerStateCounts, }); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_REMIND, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName) => { + // constructs a learner state object for the select all state to match format of select all on page metadata + const learnerStateObject = {}; + learnerStateCounts.forEach((learnerState) => { + learnerStateObject[learnerState.learnerState] = learnerState.count; + }); + + const selectedRowsMetadata = isEntireTableSelected + ? { uniqueLearnerState: learnerStateObject, totalSelectedRows: selectedRemindableRowCount } + : { + uniqueLearnerState, uniqueAssignmentState, uniqueContentKeys, totalContentQuantity, totalSelectedRows, + }; + + const trackEventMetadata = { + ...selectedRowsMetadata, + isAssignable, + isSubsidyActive, + subsidyUuid, + catalogUuid, + isEntireTableSelected, + assignmentUuids, + aggregates, + assignmentConfiguration, + isOpen: !isOpen, + }; + + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_REMIND_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_REMIND_MODAL, + ); + }; + + const reminderTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_REMIND, + ); + }; + return ( <> @@ -66,6 +143,7 @@ const AssignmentTableRemindAction = ({ AssignmentTableRemindAction.propTypes = { selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, + enterpriseId: PropTypes.string.isRequired, isEntireTableSelected: PropTypes.bool.isRequired, learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({ learnerState: PropTypes.string.isRequired, @@ -73,7 +151,12 @@ AssignmentTableRemindAction.propTypes = { })).isRequired, tableInstance: PropTypes.shape({ columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, + itemCount: PropTypes.number.isRequired, }).isRequired, }; -export default AssignmentTableRemindAction; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentTableRemindAction); diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index 3b3c7e76d1..b9d6aa7dc3 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -119,7 +119,7 @@ const BudgetAssignmentsTable = ({ EmptyTableComponent={CustomDataTableEmptyState} bulkActions={[ , - , + , ]} /> ); diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 8c41d33eef..21e65d2de1 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -21,8 +21,8 @@ const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) isTopDownAssignmentEnabled, }); - // // If the budget activity overview data is loading (either the initial request OR any - // // background re-fetching), show a skeleton. + // If the budget activity overview data is loading (either the initial request OR any + // background re-fetching), show a skeleton. if (isBudgetActivityOverviewLoading || isBudgetActivityOverviewFetching || !budgetActivityOverview) { return ( <> diff --git a/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx b/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx index fd8ae54555..5d4c9f4294 100644 --- a/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx @@ -3,27 +3,60 @@ import { connect } from 'react-redux'; import { Breadcrumb } from '@edx/paragon'; import { Link } from 'react-router-dom'; import React from 'react'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import EVENT_NAMES from '../../eventTracking'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; -const BudgetDetailPageBreadcrumbs = ({ enterpriseSlug, budgetDisplayName }) => ( -
    - -
    -); +const BudgetDetailPageBreadcrumbs = ({ enterpriseId, enterpriseSlug, budgetDisplayName }) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + const trackEventMetadata = {}; + if (subsidyAccessPolicy) { + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, catalogUuid, aggregates, isAssignable, + } = subsidyAccessPolicy; + Object.assign( + trackEventMetadata, + { + subsidyUuid, + assignmentConfiguration, + isSubsidyActive, + isAssignable, + catalogUuid, + aggregates, + }, + ); + } + + return ( +
    + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BREADCRUMB_FROM_BUDGET_DETAIL_TO_BUDGETS, + trackEventMetadata, + )} + /> +
    + ); +}; const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, enterpriseSlug: state.portalConfiguration.enterpriseSlug, }); BudgetDetailPageBreadcrumbs.propTypes = { + enterpriseId: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, budgetDisplayName: PropTypes.string.isRequired, }; diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index 39fa31dbe8..9be186816a 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -7,8 +7,10 @@ import { } from '@edx/paragon'; import { Add } from '@edx/paragon/icons'; import { generatePath, useRouteMatch, Link } from 'react-router-dom'; -import { formatPrice } from './data'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from './data'; import { configuration } from '../../config'; +import EVENT_NAMES from '../../eventTracking'; const BudgetDetail = ({ available, utilized, limit }) => { const currentProgressBarLimit = (available / limit) * 100; @@ -38,9 +40,29 @@ BudgetDetail.propTypes = { limit: PropTypes.number.isRequired, }; -const BudgetActions = ({ budgetId, isAssignable }) => { +const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => { const routeMatch = useRouteMatch(); const supportUrl = configuration.ENTERPRISE_SUPPORT_URL; + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + const trackEventMetadata = {}; + if (subsidyAccessPolicy) { + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, catalogUuid, aggregates, + } = subsidyAccessPolicy; + Object.assign( + trackEventMetadata, + { + subsidyUuid, + assignmentConfiguration, + isSubsidyActive, + isAssignable, + catalogUuid, + aggregates, + }, + ); + } const isLargeScreenOrGreater = useMediaQuery({ query: `(min-width: ${breakpoints.small.minWidth}px)` }); @@ -53,7 +75,17 @@ const BudgetActions = ({ budgetId, isAssignable }) => { Funds from this budget are set to auto-allocate to registered learners based on settings configured with your support team.

    -
    @@ -73,6 +105,11 @@ const BudgetActions = ({ budgetId, isAssignable }) => { pathname: generatePath(routeMatch.path, { budgetId, activeTabKey: 'catalog' }), state: { budgetActivityScrollToKey: 'catalog' }, }} + onClick={() => sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_OVERVIEW_NEW_ASSIGNMENT, + trackEventMetadata, + )} > New course assignment @@ -84,6 +121,7 @@ const BudgetActions = ({ budgetId, isAssignable }) => { BudgetActions.propTypes = { budgetId: PropTypes.string.isRequired, isAssignable: PropTypes.bool.isRequired, + enterpriseId: PropTypes.string.isRequired, }; const BudgetDetailPageOverviewAvailability = ({ @@ -91,6 +129,7 @@ const BudgetDetailPageOverviewAvailability = ({ isAssignable, budgetTotalSummary: { available, utilized, limit }, enterpriseFeatures, + enterpriseId, }) => ( @@ -101,6 +140,7 @@ const BudgetDetailPageOverviewAvailability = ({ @@ -120,9 +160,11 @@ BudgetDetailPageOverviewAvailability.propTypes = { enterpriseFeatures: PropTypes.shape({ topDownAssignmentRealTimeLcm: PropTypes.bool, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, }); diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx index 2276670b81..f4865abd7d 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx @@ -5,11 +5,13 @@ import { Stack, Collapsible, Row, Col, Button, } from '@edx/paragon'; import { ArrowDownward } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { generatePath, useRouteMatch, Link, } from 'react-router-dom'; import { formatPrice } from './data'; +import EVENT_NAMES from '../../eventTracking'; const BudgetDetailPageOverviewUtilization = ({ budgetId, @@ -17,10 +19,15 @@ const BudgetDetailPageOverviewUtilization = ({ budgetAggregates, isAssignable, enterpriseFeatures, + enterpriseId, }) => { const routeMatch = useRouteMatch(); - const { amountAllocatedUsd, amountRedeemedUsd } = budgetAggregates; + const { + BUDGET_OVERVIEW_UTILIZATION_VIEW_ASSIGNED_TABLE, + BUDGET_OVERVIEW_UTILIZATION_VIEW_SPENT_TABLE, + BUDGET_OVERVIEW_UTILIZATION_DROPDOWN_TOGGLE, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; if (!budgetId || !enterpriseFeatures.topDownAssignmentRealTimeLcm || utilized <= 0 || !isAssignable) { return null; @@ -32,6 +39,9 @@ const BudgetDetailPageOverviewUtilization = ({ } const linkText = (type === 'assigned') ? 'View assigned activity' : 'View spent activity'; + const eventNameType = (type === 'assigned') + ? BUDGET_OVERVIEW_UTILIZATION_VIEW_ASSIGNED_TABLE + : BUDGET_OVERVIEW_UTILIZATION_VIEW_SPENT_TABLE; return ( @@ -55,6 +69,13 @@ const BudgetDetailPageOverviewUtilization = ({ className="mt-4 budget-utilization-container" styling="basic" title={
    Utilization details
    } + onToggle={(open) => sendEnterpriseTrackEvent( + enterpriseId, + BUDGET_OVERVIEW_UTILIZATION_DROPDOWN_TOGGLE, + { + isOpen: open, + }, + )} > @@ -121,10 +142,12 @@ BudgetDetailPageOverviewUtilization.propTypes = { enterpriseFeatures: PropTypes.shape({ topDownAssignmentRealTimeLcm: PropTypes.bool, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; const mapStateToProps = state => ({ enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, + enterpriseId: state.portalConfiguration.enterpriseId, }); export default connect(mapStateToProps)(BudgetDetailPageOverviewUtilization); diff --git a/src/components/learner-credit-management/CancelAssignmentModal.jsx b/src/components/learner-credit-management/CancelAssignmentModal.jsx index cce7b896b1..13657a184b 100644 --- a/src/components/learner-credit-management/CancelAssignmentModal.jsx +++ b/src/components/learner-credit-management/CancelAssignmentModal.jsx @@ -12,6 +12,7 @@ const CancelAssignmentModal = ({ close, isOpen, uuidCount, + trackEvent, }) => { const { successfulCancellationToast: { displayToastForAssignmentCancellation }, @@ -19,6 +20,7 @@ const CancelAssignmentModal = ({ const handleOnClick = async () => { await cancelContentAssignments(); + trackEvent(); displayToastForAssignmentCancellation(uuidCount); }; @@ -69,6 +71,7 @@ CancelAssignmentModal.propTypes = { close: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, uuidCount: PropTypes.number, + trackEvent: PropTypes.func.isRequired, }; export default CancelAssignmentModal; diff --git a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx index 5b2144b149..74ebab3da9 100644 --- a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx +++ b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx @@ -4,25 +4,96 @@ import { Icon, IconButtonWithTooltip, } from '@edx/paragon'; import { DoNotDisturbOn } from '@edx/paragon/icons'; +import { connect } from 'react-redux'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; import CancelAssignmentModal from './CancelAssignmentModal'; +import EVENT_NAMES from '../../eventTracking'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; + +const PendingAssignmentCancelButton = ({ row, enterpriseId }) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; -const PendingAssignmentCancelButton = ({ row }) => { const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; + const { + contentKey, + contentQuantity, + learnerState, + state, + uuid, + } = row.original; + const { cancelButtonState, cancelContentAssignments, close, isOpen, open, - } = useCancelContentAssignments(row.original.assignmentConfiguration, [row.original.uuid]); + } = useCancelContentAssignments(assignmentConfiguration, [uuid]); + + const sharedTrackEventMetadata = { + subsidyUuid, + isSubsidyActive, + isAssignable, + catalogUuid, + assignmentConfiguration, + contentKey, + contentQuantity, + learnerState, + aggregates, + assignmentState: state, + isOpen: !isOpen, + }; + + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CANCEL, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName, eventMetadata = {}) => { + const trackEventMetadata = { + ...sharedTrackEventMetadata, + ...eventMetadata, + }; + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_CANCEL_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_CANCEL_MODAL, + ); + }; + + const cancellationTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CANCEL, + ); + }; + return ( <> { /> ); @@ -41,11 +113,20 @@ const PendingAssignmentCancelButton = ({ row }) => { PendingAssignmentCancelButton.propTypes = { row: PropTypes.shape({ original: PropTypes.shape({ + contentKey: PropTypes.string.isRequired, + contentQuantity: PropTypes.number.isRequired, + learnerState: PropTypes.string.isRequired, + state: PropTypes.string.isRequired, assignmentConfiguration: PropTypes.string.isRequired, learnerEmail: PropTypes.string, uuid: PropTypes.string.isRequired, }).isRequired, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; -export default PendingAssignmentCancelButton; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(PendingAssignmentCancelButton); diff --git a/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx b/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx index 07f38883cf..a88843a2fd 100644 --- a/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx +++ b/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx @@ -3,18 +3,88 @@ import PropTypes from 'prop-types'; import { Icon, IconButtonWithTooltip } from '@edx/paragon'; import { Mail } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import RemindAssignmentModal from './RemindAssignmentModal'; import useRemindContentAssignments from './data/hooks/useRemindContentAssignments'; +import EVENT_NAMES from '../../eventTracking'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; + +const PendingAssignmentRemindButton = ({ row, enterpriseId }) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; -const PendingAssignmentRemindButton = ({ row }) => { const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; + const { + contentKey, + contentQuantity, + learnerState, + state, + uuid, + } = row.original; + const { remindButtonState, remindContentAssignments, close, isOpen, open, - } = useRemindContentAssignments(row.original.assignmentConfiguration, [row.original.uuid]); + } = useRemindContentAssignments(assignmentConfiguration, [uuid]); + + const sharedTrackEventMetadata = { + subsidyUuid, + isSubsidyActive, + isAssignable, + catalogUuid, + assignmentConfiguration, + contentKey, + contentQuantity, + learnerState, + aggregates, + assignmentState: state, + isOpen: !isOpen, + }; + + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_REMIND, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName, eventMetadata = {}) => { + const trackEventMetadata = { + ...sharedTrackEventMetadata, + ...eventMetadata, + }; + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_REMIND_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_REMIND_MODAL, + ); + }; + + const reminderTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_REMIND, + ); + }; return ( <> @@ -22,16 +92,17 @@ const PendingAssignmentRemindButton = ({ row }) => { alt={`Remind learner ${emailAltText}`} data-testid={`remind-assignment-${row.original.uuid}`} iconAs={Icon} - onClick={open} + onClick={openModal} src={Mail} tooltipContent="Remind learner" tooltipPlacement="top" /> ); @@ -40,11 +111,20 @@ const PendingAssignmentRemindButton = ({ row }) => { PendingAssignmentRemindButton.propTypes = { row: PropTypes.shape({ original: PropTypes.shape({ + contentKey: PropTypes.string.isRequired, + contentQuantity: PropTypes.number.isRequired, + learnerState: PropTypes.string.isRequired, + state: PropTypes.string.isRequired, assignmentConfiguration: PropTypes.string.isRequired, learnerEmail: PropTypes.string, uuid: PropTypes.string.isRequired, }).isRequired, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; -export default PendingAssignmentRemindButton; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(PendingAssignmentRemindButton); diff --git a/src/components/learner-credit-management/RemindAssignmentModal.jsx b/src/components/learner-credit-management/RemindAssignmentModal.jsx index 013535e2b9..546bee7093 100644 --- a/src/components/learner-credit-management/RemindAssignmentModal.jsx +++ b/src/components/learner-credit-management/RemindAssignmentModal.jsx @@ -7,7 +7,7 @@ import { Mail } from '@edx/paragon/icons'; import { BudgetDetailPageContext } from './BudgetDetailPageWrapper'; const RemindAssignmentModal = ({ - remindButtonState, remindContentAssignments, close, isOpen, uuidCount, + remindButtonState, remindContentAssignments, close, isOpen, uuidCount, trackEvent, }) => { const { successfulReminderToast: { displayToastForAssignmentReminder }, @@ -15,6 +15,7 @@ const RemindAssignmentModal = ({ const handleOnClick = async () => { await remindContentAssignments(); + trackEvent(); displayToastForAssignmentReminder(uuidCount); }; @@ -66,6 +67,7 @@ RemindAssignmentModal.propTypes = { close: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, uuidCount: PropTypes.number, + trackEvent: PropTypes.func.isRequired, }; export default RemindAssignmentModal; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx index b6664a8768..df5fb496c3 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx @@ -1,30 +1,46 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedBadEmail = ({ learnerEmail }) => { - const [isOpen, open, close] = useToggle(false); +const FailedBadEmail = ({ learnerEmail, trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL_HELP_CENTER, + trackEvent, + }); return ( <> Failed: Bad email Failed: Bad email @@ -41,7 +57,11 @@ const FailedBadEmail = ({ learnerEmail }) => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -55,6 +75,7 @@ const FailedBadEmail = ({ learnerEmail }) => { FailedBadEmail.propTypes = { learnerEmail: PropTypes.string, + trackEvent: PropTypes.func.isRequired, }; FailedBadEmail.defaultProps = { diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx index f102592f4f..18bc683c56 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx @@ -1,29 +1,46 @@ import React, { useState } from 'react'; -import { Chip, useToggle, Hyperlink } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedCancellation = () => { - const [isOpen, open, close] = useToggle(false); +const FailedCancellation = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION_HELP_CENTER, + trackEvent, + }); return ( <> Failed: Cancellation Failed: Cancellation @@ -41,7 +58,11 @@ const FailedCancellation = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments
  • @@ -53,4 +74,8 @@ const FailedCancellation = () => { ); }; +FailedCancellation.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedCancellation; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx index 6e12f9303f..ef263065c6 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx @@ -1,29 +1,48 @@ import React, { useState } from 'react'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import PropTypes from 'prop-types'; + +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedRedemption = () => { - const [isOpen, open, close] = useToggle(false); +const FailedRedemption = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION_HELP_CENTER, + trackEvent, + }); + return ( <> Failed: Redemption Failed: Redemption @@ -44,7 +63,11 @@ const FailedRedemption = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -56,4 +79,8 @@ const FailedRedemption = () => { ); }; +FailedRedemption.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedRedemption; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx index 033ae7bca4..03519222c9 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx @@ -1,27 +1,45 @@ import React, { useState } from 'react'; -import { Chip, useToggle, Hyperlink } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedReminder = () => { - const [isOpen, open, close] = useToggle(false); +const FailedReminder = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER_HELP_CENTER, + trackEvent, + }); return ( <> Failed: Reminder Failed: Reminder @@ -39,7 +57,11 @@ const FailedReminder = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -51,4 +73,8 @@ const FailedReminder = () => { ); }; +FailedReminder.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedReminder; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx index 43ae45e4b0..c5a1acb026 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx @@ -1,29 +1,47 @@ import React, { useState } from 'react'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import PropTypes from 'prop-types'; + +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedSystem = () => { - const [isOpen, open, close] = useToggle(false); +const FailedSystem = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM_HELP_CENTER, + trackEvent, + }); return ( <> Failed: System Failed: System @@ -38,7 +56,11 @@ const FailedSystem = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -50,4 +72,8 @@ const FailedSystem = () => { ); }; +FailedSystem.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedSystem; diff --git a/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx index 2380fda362..94ac11d39f 100644 --- a/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx @@ -1,28 +1,39 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Chip, useToggle } from '@edx/paragon'; +import { Chip } from '@edx/paragon'; import { Send } from '@edx/paragon/icons'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const NotifyingLearner = ({ learnerEmail }) => { - const [isOpen, open, close] = useToggle(false); +const NotifyingLearner = ({ learnerEmail, trackEvent }) => { const [target, setTarget] = useState(null); + const { BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_NOTIFY_LEARNER } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_NOTIFY_LEARNER, + trackEvent, + }); return ( <> Notifying learner Notifying {learnerEmail ?? 'learner'} @@ -40,6 +51,7 @@ const NotifyingLearner = ({ learnerEmail }) => { NotifyingLearner.propTypes = { learnerEmail: PropTypes.string, + trackEvent: PropTypes.func.isRequired, }; export default NotifyingLearner; diff --git a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx index f1176b3317..819f918c53 100644 --- a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx @@ -1,31 +1,46 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Timelapse } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; -import { ASSIGNMENT_ENROLLMENT_DEADLINE } from '../data'; +import { ASSIGNMENT_ENROLLMENT_DEADLINE, useAssignmentStatusChip } from '../data'; +import EVENT_NAMES from '../../../eventTracking'; -const WaitingForLearner = ({ learnerEmail }) => { - const [isOpen, open, close] = useToggle(false); +const WaitingForLearner = ({ learnerEmail, trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER_HELP_CENTER, + trackEvent, + }); return ( <> Waiting for learner Waiting for {learnerEmail ?? 'learner'} @@ -40,7 +55,11 @@ const WaitingForLearner = ({ learnerEmail }) => {

    Need help?

    Learn more about learner enrollment in assigned courses at{' '} - + Help Center: Course Assignments .

    @@ -53,6 +72,7 @@ const WaitingForLearner = ({ learnerEmail }) => { WaitingForLearner.propTypes = { learnerEmail: PropTypes.string, + trackEvent: PropTypes.func.isRequired, }; export default WaitingForLearner; diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 790fc68d3f..4024990c9f 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -47,7 +47,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { } = useContext(BudgetDetailPageContext); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const { - subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, } = subsidyAccessPolicy; const sharedEnterpriseTrackEventMetadata = { subsidyAccessPolicyId, @@ -55,10 +55,11 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { subsidyUuid, isSubsidyActive, isAssignable, + aggregates, contentPriceCents: course.normalizedMetadata.contentPrice * 100, contentKey: course.key, courseUuid: course.uuid, - assignmentConfigurationUuid: assignmentConfiguration.uuid, + assignmentConfiguration, }; const { mutate } = useAllocateContentAssignments(); @@ -94,11 +95,13 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const onSuccessEnterpriseTrackEvents = ({ totalLearnersAllocated, totalLearnersAlreadyAllocated, + response, }) => { const trackEventMetadata = { ...sharedEnterpriseTrackEventMetadata, totalLearnersAllocated, totalLearnersAlreadyAllocated, + response, }; sendEnterpriseTrackEvent( enterpriseId, @@ -120,7 +123,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { setAssignButtonState('pending'); setCreateAssignmentsErrorReason(null); mutate(mutationArgs, { - onSuccess: ({ created, noChange, updated }) => { + onSuccess: (res) => { setAssignButtonState('complete'); // Ensure the budget and budgets queries are invalidated so that the relevant // queries become stale and refetches new updated data from the API. @@ -131,11 +134,12 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseId), }); handleCloseAssignmentModal(); - const totalLearnersAllocated = created.length + updated.length; - const totalLearnersAlreadyAllocated = noChange.length; + const totalLearnersAllocated = res.created.length + res.updated.length; + const totalLearnersAlreadyAllocated = res.noChange.length; onSuccessEnterpriseTrackEvents({ totalLearnersAllocated, totalLearnersAlreadyAllocated, + res, }); displayToastForAssignmentAllocation({ totalLearnersAllocated, @@ -167,6 +171,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { totalAllocatedLearners: learnerEmails.length, errorStatus: httpErrorStatus, errorReason, + response: err, }, ); }, @@ -185,7 +190,10 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_EXIT, - { assignButtonState }, + { + ...sharedEnterpriseTrackEventMetadata, + assignButtonState, + }, ); }} footerNode={( @@ -196,6 +204,10 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { onClick={() => sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_HELP_CENTER, + { + ...sharedEnterpriseTrackEventMetadata, + assignButtonState, + }, )} destination={getConfig().ENTERPRISE_SUPPORT_LEARNER_CREDIT_URL} showLaunchIcon @@ -211,7 +223,10 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_CANCEL, - { assignButtonState }, + { + ...sharedEnterpriseTrackEventMetadata, + assignButtonState, + }, ); }} > diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 517f14fbd5..f5fed6a990 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -9,7 +9,6 @@ import { QueryClientProvider, useQueryClient } from '@tanstack/react-query'; import { AppContext } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; - import CourseCard from '../CourseCard'; import { formatPrice, diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index 63681a4030..396153dad6 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -12,3 +12,4 @@ export { default as useSuccessfulAssignmentToastContextValue } from './useSucces export { default as useSuccessfulCancellationToastContextValue } from './useSuccessfulCancellationToastContextValue'; export { default as useSuccessfulReminderToastContextValue } from './useSuccessfulReminderToastContextValue'; export { default as useEnterpriseOffer } from './useEnterpriseOffer'; +export { default as useAssignmentStatusChip } from './useAssignmentStatusChip'; diff --git a/src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx b/src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx new file mode 100644 index 0000000000..f74dc90a2a --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx @@ -0,0 +1,39 @@ +import { useToggle } from '@edx/paragon'; + +/** + * + * @param chipInteractionEventName {String} - The event name that will be read in Segment of a chip opening and closing + * @param chipHelpCenterEventName {String} - The event name that will be read in Segment for a help center link + * interaction + * @param trackEvent {Function} - The track event functioning that will be sending a track event + * @returns {{ + * closeChipModal: closeChipModal, + * openChipModal: openChipModal, + * helpCenterTrackEvent: helpCenterTrackEvent, + * isChipModalOpen: * + * }} + */ +export default function useAssignmentStatusChip({ chipInteractionEventName, chipHelpCenterEventName, trackEvent }) { + const [isChipModalOpen, open, close] = useToggle(false); + const openChipModal = () => { + open(); + // Note: could hardcode true given this function *always* opens + trackEvent(chipInteractionEventName, { isOpen: true }); + }; + const closeChipModal = () => { + close(); + // Note: could hardcode false given this function *always* closes + trackEvent(chipInteractionEventName, { isOpen: false }); + }; + + const helpCenterTrackEvent = () => { + trackEvent(chipHelpCenterEventName); + }; + + return { + isChipModalOpen, + openChipModal, + closeChipModal, + helpCenterTrackEvent, + }; +} diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 51746f3eca..2216a01580 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -337,3 +337,60 @@ export async function retrieveBudgetDetailActivityOverview({ } return result; } + +/** + * Takes the raw selected flat rows data from the 'Assigned' datatable and returns metadata that is used for tracking + * bulk enrollment of reminders and bulk enrollment of cancellations. + * @param {Array} selectedFlatRows An array of selectedFlatRows from the activity 'Assigned' table + * @returns {{ + * uniqueLearnerState: [String], + * totalContentQuantity: Number, + * assignmentConfigurationUuid: String, + * assignmentUuids: [String] + * uniqueContentKeys: [String], + * uniqueAssignmentState: [String], + * totalSelectedRows: Number, + * }} + */ +export const transformSelectedRows = (selectedFlatRows) => { + const assignmentUuids = selectedFlatRows.map(item => item.id); + const totalSelectedRows = selectedFlatRows.length; + + // Count of unique content keys, where the key is the course, + // and value is count of the course. + const flatMappedContentKeys = selectedFlatRows.map(item => item?.original?.contentKey); + const uniqueContentKeys = {}; + flatMappedContentKeys.forEach((courseKey) => { + uniqueContentKeys[courseKey] = (uniqueContentKeys[courseKey] || 0) + 1; + }); + + // Count of unique learner states, where the key is the learnerState, + // and value is count of the learnerState. + const flatMappedLearnerState = selectedFlatRows.map(item => item?.original?.learnerState); + const uniqueLearnerState = {}; + flatMappedLearnerState.forEach((learnerState) => { + uniqueLearnerState[learnerState] = (uniqueLearnerState[learnerState] || 0) + 1; + }); + + // Count of unique assignment states, where the key is the assignment state, + // and value is count of the assignment state. + const flatMappedAssignmentState = selectedFlatRows.map(item => item?.original?.state); + const uniqueAssignmentState = {}; + flatMappedAssignmentState.forEach((state) => { + uniqueAssignmentState[state] = (uniqueAssignmentState[state] || 0) + 1; + }); + + // Total value of all the selected rows accumulated from the contentQuantity + const totalContentQuantity = selectedFlatRows.map( + item => item.original.contentQuantity, + ).reduce((prev, next) => prev + next, 0); + + return { + uniqueAssignmentState, + uniqueLearnerState, + uniqueContentKeys, + totalContentQuantity, + assignmentUuids, + totalSelectedRows, + }; +}; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 9052c8d93c..b3ade09d7b 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -1175,10 +1175,25 @@ describe('', () => { expect(statusChip).toBeInTheDocument(); userEvent.click(statusChip); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + // Modal popup is visible with expected text const modalPopupContents = within(screen.getByTestId('assignment-status-modalpopup-contents')); expect(modalPopupContents.getByText(expectedModalPopupHeading)).toBeInTheDocument(); expect(modalPopupContents.getByText(expectedModalPopupContent, { exact: false })).toBeInTheDocument(); + + // Help Center link clicked and modal closed + if (screen.queryByText('Help Center: Course Assignments')) { + const helpCenterLink = screen.getByText('Help Center: Course Assignments'); + userEvent.click(helpCenterLink); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + // Click chip to close modal + userEvent.click(statusChip); + expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); + } else { + userEvent.click(statusChip); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + } }); it.each([ @@ -1332,6 +1347,8 @@ describe('', () => { userEvent.click(catalogTab); }); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + await waitFor(() => { expect(screen.getByTestId('budget-detail-catalog-tab-contents')).toBeInTheDocument(); }); @@ -1516,6 +1533,7 @@ describe('', () => { const cancelBulkActionButton = screen.getByText('Cancel (2)'); expect(cancelBulkActionButton).toBeInTheDocument(); userEvent.click(cancelBulkActionButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const modalDialog = screen.getByRole('dialog'); expect(modalDialog).toBeInTheDocument(); const cancelDialogButton = getButtonElement('Cancel assignments (2)'); @@ -1528,6 +1546,7 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Assignments canceled (2)')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); it('reminds assignments in bulk', async () => { @@ -1608,6 +1627,7 @@ describe('', () => { expect(modalDialog).toBeInTheDocument(); const remindDialogButton = getButtonElement('Send reminders (2)'); userEvent.click(remindDialogButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); await waitFor( () => expect( EnterpriseAccessApiService.remindAllContentAssignments, @@ -1616,6 +1636,7 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Reminders sent (2)')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); it('cancels a single assignment', async () => { @@ -1666,6 +1687,7 @@ describe('', () => { const cancelIconButton = screen.getByTestId('cancel-assignment-test-uuid'); expect(cancelIconButton).toBeInTheDocument(); userEvent.click(cancelIconButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const modalDialog = screen.getByRole('dialog'); expect(modalDialog).toBeInTheDocument(); const cancelDialogButton = getButtonElement('Cancel assignment'); @@ -1673,6 +1695,7 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Assignment canceled')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); it('reminds a single assignment', async () => { EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 }); @@ -1722,6 +1745,7 @@ describe('', () => { const remindIconButton = screen.getByTestId('remind-assignment-test-uuid'); expect(remindIconButton).toBeInTheDocument(); userEvent.click(remindIconButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const modalDialog = screen.getByRole('dialog'); expect(modalDialog).toBeInTheDocument(); const remindDialogButton = getButtonElement('Send reminder'); @@ -1729,5 +1753,6 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Reminder sent')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); }); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx index 96c7ed5fc2..7ce956ab94 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx @@ -7,9 +7,12 @@ import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import '@testing-library/jest-dom/extend-expect'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import BudgetDetailPageWrapper, { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; -import { getButtonElement } from '../../test/testUtils'; +import { getButtonElement, queryClient } from '../../test/testUtils'; +import BudgetDetailPageBreadcrumbs from '../BudgetDetailPageBreadcrumbs'; const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); @@ -26,19 +29,27 @@ const defaultStoreState = { }, }; +jest.mock('@edx/frontend-enterprise-utils', () => ({ + ...jest.requireActual('@edx/frontend-enterprise-utils'), + sendEnterpriseTrackEvent: jest.fn(), +})); + const MockBudgetDetailPageWrapper = ({ initialStoreState = defaultStoreState, children, }) => { const store = getMockStore(initialStoreState); return ( - - - - {children} - - - + + + + + {children} + + + + + ); }; @@ -263,4 +274,16 @@ describe('', () => { expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); }); }); + it('calls segment event on breadcrumb click', () => { + const mockBudgetDisplayName = 'Test Budget'; + renderWithRouter( + + + , + ); + const previousBreadcrumb = screen.getByText('Budgets'); + + userEvent.click(previousBreadcrumb); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/eventTracking.js b/src/eventTracking.js index ed4244786f..d77196db3e 100644 --- a/src/eventTracking.js +++ b/src/eventTracking.js @@ -103,14 +103,54 @@ export const SUBSCRIPTION_EVENTS = { }; export const LEARNER_CREDIT_MANAGEMENT_EVENTS = { + BREADCRUMB_FROM_BUDGET_DETAIL_TO_BUDGETS: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.breadcrumb_budget_detail_to_budgets.clicked`, TAB_CHANGED: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.tab.changed`, + // Budget Overview + BUDGET_OVERVIEW_CONTACT_US: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.contact_us.clicked`, + BUDGET_OVERVIEW_NEW_ASSIGNMENT: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.new_assignment.clicked`, + BUDGET_OVERVIEW_UTILIZATION_DROPDOWN_TOGGLE: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.utilization_dropdown.toggled`, + BUDGET_OVERVIEW_UTILIZATION_VIEW_SPENT_TABLE: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.view_spent_activity.clicked`, + BUDGET_OVERVIEW_UTILIZATION_VIEW_ASSIGNED_TABLE: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.view_assigned_activity.clicked`, // Activity tab + // Activity tab assigned datatable BUDGET_DETAILS_ASSIGNED_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table.changed`, BUDGET_DETAILS_ASSIGNED_DATATABLE_VIEW_COURSE: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_view_course.clicked`, BUDGET_DETAILS_ASSIGNED_DATATABLE_ACTIONS_REFRESH: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_refresh.clicked`, + // Activity tab assigned table remind + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_remind_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_remind_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_REMIND: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_remind.clicked`, + // Activity tab assigned table bulk remind + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_remind_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_remind_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_REMIND: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_remind.clicked`, + // Activity tab assigned table cancel + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_cancel_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_cancel_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CANCEL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_cancel.clicked`, + // Activity tab assigned table bulk cancel + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_cancel_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_cancel_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_cancel.clicked`, + // Activity tab spent datatable BUDGET_DETAILS_SPENT_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.spent_table.changed`, BUDGET_DETAILS_SPENT_DATATABLE_VIEW_COURSE: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.spent_table_view_course.clicked`, EMPTY_STATE_CTA: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.empty_state_cta_to_catalog.clicked`, + // Activity tab chips + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_NOTIFY_LEARNER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_notify_learner.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_waiting_for_learner.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_cancellation.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_system.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_bad_email.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_reminder.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_redemption.clicked`, + // Activity tab chips help center links + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_waiting_for_learner_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_cancellation_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_system_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_bad_email_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_reminder_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_redemption_help_center.clicked`, // Catalog tab // Catalog tab search VIEW_COURSE: `${BUDGET_DETAIL_SEARCH_PREFIX}.view_course.clicked`, From dbdbe10d18c1517f1a4767ed0de9ebb9c1b6dff7 Mon Sep 17 00:00:00 2001 From: Alex Dusenbery Date: Tue, 2 Jan 2024 11:18:46 -0500 Subject: [PATCH 120/124] feat: apply filters to cancel/remind all ENT-8156 | Pass the appropriate content assignment filter state to the `cancel-all` and `remind-all` endpoints. --- src/components/forms/FormWorkflow.tsx | 4 +- .../AssignmentTableCancel.jsx | 18 ++++---- .../AssignmentTableRemind.jsx | 18 ++++---- .../PendingAssignmentCancelButton.jsx | 6 ++- .../PendingAssignmentRemindButton.jsx | 6 ++- .../data/hooks/useCancelContentAssignments.js | 10 +++-- .../useCancelContentAssignments.test.jsx | 41 +++++++++++++++++++ .../data/hooks/useRemindContentAssignments.js | 10 +++-- .../useRemindContentAssignments.test.jsx | 41 +++++++++++++++++++ .../steps/NewSSOConfigConfigureStep.tsx | 2 +- .../steps/NewSSOConfigConnectStep.tsx | 4 +- .../services/EnterpriseAccessApiService.js | 30 ++++++++++++-- .../tests/EnterpriseAccessApiService.test.js | 14 +++++-- 13 files changed, 168 insertions(+), 36 deletions(-) diff --git a/src/components/forms/FormWorkflow.tsx b/src/components/forms/FormWorkflow.tsx index a60b017e64..88688afec1 100644 --- a/src/components/forms/FormWorkflow.tsx +++ b/src/components/forms/FormWorkflow.tsx @@ -138,8 +138,8 @@ const FormWorkflow = ({ const newFormFields: FormConfigData = await nextButtonConfig.onClick({ formFields, errHandler: (error) => { - setFormError(error); - if (!!error) { + setFormError(error); + if (error) { advance = false; } }, diff --git a/src/components/learner-credit-management/AssignmentTableCancel.jsx b/src/components/learner-credit-management/AssignmentTableCancel.jsx index eac61c8f31..ad376a31d8 100644 --- a/src/components/learner-credit-management/AssignmentTableCancel.jsx +++ b/src/components/learner-credit-management/AssignmentTableCancel.jsx @@ -8,7 +8,6 @@ import CancelAssignmentModal from './CancelAssignmentModal'; import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; import { transformSelectedRows, useBudgetId, useSubsidyAccessPolicy } from './data'; import EVENT_NAMES from '../../eventTracking'; -import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToCancel = ({ assignmentUuids, @@ -39,10 +38,7 @@ const AssignmentTableCancelAction = ({ totalSelectedRows, } = transformSelectedRows(selectedFlatRows); - const activeFilters = getActiveTableColumnFilters(tableInstance.columns); - - // If entire table is selected and there are NO filters, hit cancel-all endpoint. Otherwise, hit usual bulk cancel. - const shouldCancelAll = isEntireTableSelected && activeFilters.length === 0; + const { state: dataTableState } = tableInstance; const { cancelButtonState, @@ -50,7 +46,12 @@ const AssignmentTableCancelAction = ({ close, isOpen, open, - } = useCancelContentAssignments(assignmentConfiguration.uuid, assignmentUuids, shouldCancelAll); + } = useCancelContentAssignments( + assignmentConfiguration.uuid, + assignmentUuids, + isEntireTableSelected, + dataTableState.filters, + ); const { BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL, @@ -114,7 +115,7 @@ const AssignmentTableCancelAction = ({ const tableItemCount = tableInstance.itemCount; const totalToCancel = calculateTotalToCancel({ assignmentUuids, - isEntireTableSelected: shouldCancelAll, + isEntireTableSelected, tableItemCount, }); @@ -146,6 +147,9 @@ AssignmentTableCancelAction.propTypes = { tableInstance: PropTypes.shape({ itemCount: PropTypes.number.isRequired, columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, + state: PropTypes.shape({ + filters: PropTypes.arrayOf(PropTypes.shape()).isRequired, + }).isRequired, }).isRequired, }; diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx index d4eb2f00f1..5c2de29a36 100644 --- a/src/components/learner-credit-management/AssignmentTableRemind.jsx +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -8,7 +8,6 @@ import useRemindContentAssignments from './data/hooks/useRemindContentAssignment import RemindAssignmentModal from './RemindAssignmentModal'; import { transformSelectedRows, useBudgetId, useSubsidyAccessPolicy } from './data'; import EVENT_NAMES from '../../eventTracking'; -import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToRemind = ({ assignmentUuids, @@ -41,10 +40,7 @@ const AssignmentTableRemindAction = ({ totalSelectedRows, } = transformSelectedRows(remindableRows); - const activeFilters = getActiveTableColumnFilters(tableInstance.columns); - - // If entire table is selected and there are NO filters, hit remind-all endpoint. Otherwise, hit usual bulk remind. - const shouldRemindAll = isEntireTableSelected && activeFilters.length === 0; + const { state: dataTableState } = tableInstance; const { remindButtonState, @@ -52,11 +48,16 @@ const AssignmentTableRemindAction = ({ close, isOpen, open, - } = useRemindContentAssignments(assignmentConfiguration.uuid, assignmentUuids, shouldRemindAll); + } = useRemindContentAssignments( + assignmentConfiguration.uuid, + assignmentUuids, + isEntireTableSelected, + dataTableState.filters, + ); const selectedRemindableRowCount = calculateTotalToRemind({ assignmentUuids, - isEntireTableSelected: shouldRemindAll, + isEntireTableSelected, learnerStateCounts, }); @@ -152,6 +153,9 @@ AssignmentTableRemindAction.propTypes = { tableInstance: PropTypes.shape({ columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, itemCount: PropTypes.number.isRequired, + state: PropTypes.shape({ + filters: PropTypes.arrayOf(PropTypes.shape()).isRequired, + }).isRequired, }).isRequired, }; diff --git a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx index 74ebab3da9..ffd9e0d508 100644 --- a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx +++ b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx @@ -33,7 +33,7 @@ const PendingAssignmentCancelButton = ({ row, enterpriseId }) => { close, isOpen, open, - } = useCancelContentAssignments(assignmentConfiguration, [uuid]); + } = useCancelContentAssignments(assignmentConfiguration.uuid, [uuid]); const sharedTrackEventMetadata = { subsidyUuid, @@ -117,7 +117,9 @@ PendingAssignmentCancelButton.propTypes = { contentQuantity: PropTypes.number.isRequired, learnerState: PropTypes.string.isRequired, state: PropTypes.string.isRequired, - assignmentConfiguration: PropTypes.string.isRequired, + assignmentConfiguration: PropTypes.shape({ + uuid: PropTypes.string.isRequired, + }).isRequired, learnerEmail: PropTypes.string, uuid: PropTypes.string.isRequired, }).isRequired, diff --git a/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx b/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx index a88843a2fd..7cc7403b45 100644 --- a/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx +++ b/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx @@ -32,7 +32,7 @@ const PendingAssignmentRemindButton = ({ row, enterpriseId }) => { close, isOpen, open, - } = useRemindContentAssignments(assignmentConfiguration, [uuid]); + } = useRemindContentAssignments(assignmentConfiguration.uuid, [uuid]); const sharedTrackEventMetadata = { subsidyUuid, @@ -115,7 +115,9 @@ PendingAssignmentRemindButton.propTypes = { contentQuantity: PropTypes.number.isRequired, learnerState: PropTypes.string.isRequired, state: PropTypes.string.isRequired, - assignmentConfiguration: PropTypes.string.isRequired, + assignmentConfiguration: PropTypes.shape({ + uuid: PropTypes.string.isRequired, + }).isRequired, learnerEmail: PropTypes.string, uuid: PropTypes.string.isRequired, }).isRequired, diff --git a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js index a21dc68c64..b7edc97823 100644 --- a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js +++ b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.js @@ -6,11 +6,13 @@ import { useToggle } from '@edx/paragon'; import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys } from '../constants'; import useBudgetId from './useBudgetId'; +import { applyFiltersToOptions } from './useBudgetContentAssignments'; const useCancelContentAssignments = ( assignmentConfigurationUuid, assignmentUuids, - cancelAll = false, + cancelAll, + tableFilters, ) => { const [isOpen, open, close] = useToggle(false); const [cancelButtonState, setCancelButtonState] = useState('default'); @@ -21,7 +23,9 @@ const useCancelContentAssignments = ( setCancelButtonState('pending'); try { if (cancelAll) { - await EnterpriseAccessApiService.cancelAllContentAssignments(assignmentConfigurationUuid); + const options = {}; + applyFiltersToOptions(tableFilters, options); + await EnterpriseAccessApiService.cancelAllContentAssignments(assignmentConfigurationUuid, options); } else { await EnterpriseAccessApiService.cancelContentAssignments(assignmentConfigurationUuid, assignmentUuids); } @@ -33,7 +37,7 @@ const useCancelContentAssignments = ( logError(err); setCancelButtonState('error'); } - }, [assignmentConfigurationUuid, assignmentUuids, cancelAll, queryClient, subsidyAccessPolicyId]); + }, [assignmentConfigurationUuid, assignmentUuids, cancelAll, tableFilters, queryClient, subsidyAccessPolicyId]); return { cancelButtonState, diff --git a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx index e8bcad9513..c78e8e4d1c 100644 --- a/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useCancelContentAssignments.test.jsx @@ -104,6 +104,47 @@ describe('useCancelContentAssignments', () => { }); }); + it('should send a post request to cancel all assignments with filters', async () => { + EnterpriseAccessApiService.cancelAllContentAssignments.mockResolvedValueOnce({ status: 200 }); + const tableFilters = [{ + id: 'learnerState', + value: ['waiting'], + }]; + const cancelAll = true; + const { result } = renderHook( + () => useCancelContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + [TEST_PENDING_ASSIGNMENT_UUID_1, TEST_PENDING_ASSIGNMENT_UUID_2], + cancelAll, + tableFilters, + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + cancelButtonState: 'default', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => act(() => result.current.cancelContentAssignments())); + const expectedFilterParams = { learnerState: 'waiting' }; + expect( + EnterpriseAccessApiService.cancelAllContentAssignments, + ).toHaveBeenCalledWith(TEST_ASSIGNMENT_CONFIGURATION_UUID, expectedFilterParams); + expect(logError).toBeCalledTimes(0); + + expect(result.current).toEqual({ + cancelButtonState: 'complete', + cancelContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); + it('should handle assignment cancellation error', async () => { const error = new Error('An error occurred'); EnterpriseAccessApiService.cancelContentAssignments.mockRejectedValueOnce(error); diff --git a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js index aaeac80c36..8c70e6ef3c 100644 --- a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js +++ b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.js @@ -6,11 +6,13 @@ import { useToggle } from '@edx/paragon'; import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; import { learnerCreditManagementQueryKeys } from '../constants'; import useBudgetId from './useBudgetId'; +import { applyFiltersToOptions } from './useBudgetContentAssignments'; const useRemindContentAssignments = ( assignmentConfigurationUuid, assignmentUuids, - remindAll = false, + remindAll, + tableFilters, ) => { const [isOpen, open, close] = useToggle(false); const [remindButtonState, setRemindButtonState] = useState('default'); @@ -21,7 +23,9 @@ const useRemindContentAssignments = ( setRemindButtonState('pending'); try { if (remindAll) { - await EnterpriseAccessApiService.remindAllContentAssignments(assignmentConfigurationUuid); + const options = {}; + applyFiltersToOptions(tableFilters, options); + await EnterpriseAccessApiService.remindAllContentAssignments(assignmentConfigurationUuid, options); } else { await EnterpriseAccessApiService.remindContentAssignments(assignmentConfigurationUuid, assignmentUuids); } @@ -33,7 +37,7 @@ const useRemindContentAssignments = ( logError(err); setRemindButtonState('error'); } - }, [assignmentConfigurationUuid, assignmentUuids, remindAll, queryClient, subsidyAccessPolicyId]); + }, [assignmentConfigurationUuid, assignmentUuids, remindAll, tableFilters, queryClient, subsidyAccessPolicyId]); return { remindButtonState, diff --git a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx index 88133f99f1..daadc1ea7b 100644 --- a/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx +++ b/src/components/learner-credit-management/data/hooks/useRemindContentAssignments.test.jsx @@ -104,6 +104,47 @@ describe('useRemindContentAssignments', () => { }); }); + it('should send a post request to remind all assignments with filters', async () => { + EnterpriseAccessApiService.remindAllContentAssignments.mockResolvedValueOnce({ status: 200 }); + const tableFilters = [{ + id: 'learnerState', + value: ['waiting'], + }]; + const remindAll = true; + const { result } = renderHook( + () => useRemindContentAssignments( + TEST_ASSIGNMENT_CONFIGURATION_UUID, + [TEST_PENDING_ASSIGNMENT_UUID_1, TEST_PENDING_ASSIGNMENT_UUID_2], + remindAll, + tableFilters, + ), + { wrapper }, + ); + + expect(result.current).toEqual({ + remindButtonState: 'default', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + + await waitFor(() => result.current.remindContentAssignments()); + const expectedFilterParams = { learnerState: 'waiting' }; + expect( + EnterpriseAccessApiService.remindAllContentAssignments, + ).toHaveBeenCalledWith(TEST_ASSIGNMENT_CONFIGURATION_UUID, expectedFilterParams); + expect(logError).toBeCalledTimes(0); + + expect(result.current).toEqual({ + remindButtonState: 'complete', + remindContentAssignments: expect.any(Function), + close: expect.any(Function), + isOpen: false, + open: expect.any(Function), + }); + }); + it('should handle assignment reminder error', async () => { const error = new Error('An error occurred'); EnterpriseAccessApiService.remindContentAssignments.mockRejectedValueOnce(error); diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx index cd2a6e1efe..5ba011a48e 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx @@ -166,7 +166,7 @@ const SSOConfigConfigureStep = () => { const returnToConnectStep = () => { const connectStep = allSteps?.[0] as FormWorkflowStep; dispatch?.( - setStepAction({ step: connectStep }) + setStepAction({ step: connectStep }), ); }; diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx index bd0139db9a..d37d6d152d 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConnectStep.tsx @@ -1,5 +1,7 @@ import React, { useState } from 'react'; -import { Container, Dropzone, Form, Stack } from '@edx/paragon'; +import { + Container, Dropzone, Form, Stack, +} from '@edx/paragon'; import ValidatedFormRadio from '../../../forms/ValidatedFormRadio'; import ValidatedFormControl from '../../../forms/ValidatedFormControl'; diff --git a/src/data/services/EnterpriseAccessApiService.js b/src/data/services/EnterpriseAccessApiService.js index 7313698db4..84f4bab6ce 100644 --- a/src/data/services/EnterpriseAccessApiService.js +++ b/src/data/services/EnterpriseAccessApiService.js @@ -187,8 +187,19 @@ class EnterpriseAccessApiService { /** * Cancel ALL content assignments for a specific AssignmentConfiguration. */ - static cancelAllContentAssignments(assignmentConfigurationUUID) { - const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/cancel-all/`; + static cancelAllContentAssignments(assignmentConfigurationUUID, options = {}) { + const { learnerState, ...optionsRest } = options; + const params = { + ...snakeCaseObject(optionsRest), + }; + if (learnerState) { + params.learner_state__in = learnerState; + } + const urlParams = new URLSearchParams(params); + let url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/cancel-all/`; + if (Object.keys(params).length > 0) { + url += `?${urlParams.toString()}`; + } return EnterpriseAccessApiService.apiClient().post(url); } @@ -206,8 +217,19 @@ class EnterpriseAccessApiService { /** * Remind ALL content assignments for a specific AssignmentConfiguration. */ - static remindAllContentAssignments(assignmentConfigurationUUID) { - const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/remind-all/`; + static remindAllContentAssignments(assignmentConfigurationUUID, options = {}) { + const { learnerState, ...optionsRest } = options; + const params = { + ...snakeCaseObject(optionsRest), + }; + if (learnerState) { + params.learner_state__in = learnerState; + } + const urlParams = new URLSearchParams(params); + let url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/remind-all/`; + if (Object.keys(params).length > 0) { + url += `?${urlParams.toString()}`; + } return EnterpriseAccessApiService.apiClient().post(url); } diff --git a/src/data/services/tests/EnterpriseAccessApiService.test.js b/src/data/services/tests/EnterpriseAccessApiService.test.js index 864ef63277..c32fbeeaa9 100644 --- a/src/data/services/tests/EnterpriseAccessApiService.test.js +++ b/src/data/services/tests/EnterpriseAccessApiService.test.js @@ -213,16 +213,22 @@ describe('EnterpriseAccessApiService', () => { }); test('cancelAllContentAssignments calls enterprise-access cancel-all POST API to cancel all assignments', () => { - EnterpriseAccessApiService.cancelAllContentAssignments(mockAssignmentConfigurationUUID); + const options = { + learnerState: 'pending,waiting', + }; + EnterpriseAccessApiService.cancelAllContentAssignments(mockAssignmentConfigurationUUID, options); expect(axios.post).toBeCalledWith( - `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/cancel-all/`, + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/cancel-all/?learner_state__in=pending%2Cwaiting`, ); }); test('remindAllContentAssignments calls enterprise-access remind-all POST API to remind all learners', () => { - EnterpriseAccessApiService.remindAllContentAssignments(mockAssignmentConfigurationUUID); + const options = { + learnerState: 'pending,waiting', + }; + EnterpriseAccessApiService.remindAllContentAssignments(mockAssignmentConfigurationUUID, options); expect(axios.post).toBeCalledWith( - `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/remind-all/`, + `${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/remind-all/?learner_state__in=pending%2Cwaiting`, ); }); }); From 17972097baa1e0b1592a9efb9de9dff1b82ac7d6 Mon Sep 17 00:00:00 2001 From: Mashal Malik <107556986+Mashal-m@users.noreply.github.com> Date: Fri, 5 Jan 2024 19:03:31 +0500 Subject: [PATCH 121/124] feat: update react & react-dom to v17 (#1009) * feat: update react & react-dom to v17 * chore: replace react truncate w react-lines-ellipsis * fix: update snapshots * build: set helmet w named component * build: update jest pkgs * build: update pkgs * fix: fix test and lint * build: update test pkg * fix: fix test * refactor: update package.json file * build: update lock file * fix: fix test * refactor: update test file * fix: fix test in numbercard * fix: fix test in content highlight card * fix: fix failing test that is coming from paragon upgrade * fix: fix test * refactor: add comment in test file * fix: fix conflicts * fix: update snapshots * build: pin packages * build: pin packages * refactor: replace LinesEllipsis with paragon truncate * refactor: remove extra expect * build: uninstall react-lines-ellipsis * refactor: edit authorization test file * refactor: removed setTimeout from AuthorizatiionsConfigs * refactor: reverted AuthorizationsConfigs test * refactor: updated util getBudgetStatus and respective test to resolve test failure * fix: fix test and remove caret from pkg * fix: fix lint * fix: fix indentation * fix: fix lint error * fix: update snapshots * fix: upgrade paragon and fix test accordingly * fix: update snapshots * build: update pkg json file * fix: update test file * fix: update test file * build: update edx brand * build: remove unused pkg * fix: fix failed test and remove caret * refactor: update test script --------- Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com> Co-authored-by: Muhammad Abdullah Waheed <42172960+abdullahwaheed@users.noreply.github.com> --- package-lock.json | 6929 ++++++----------- package.json | 25 +- src/components/Admin/index.jsx | 2 +- src/components/BrandStyles/index.jsx | 2 +- src/components/CodeManagement/index.jsx | 2 +- .../ContentHighlightCardItem.jsx | 12 +- src/components/EnterpriseList/index.jsx | 2 +- src/components/ErrorPage/index.jsx | 2 +- src/components/ForbiddenPage/index.jsx | 2 +- src/components/NotFoundPage/index.jsx | 2 +- src/components/NumberCard/NumberCard.test.jsx | 4 +- src/components/RequestCodesPage/index.jsx | 2 +- .../tests/BudgetDetailPage.test.jsx | 2 +- .../tests/EmailAddressTableCell.test.jsx | 4 +- .../tests/AuthorizationsConfigs.test.tsx | 13 +- src/setupTest.js | 2 +- 16 files changed, 2318 insertions(+), 4689 deletions(-) diff --git a/package-lock.json b/package-lock.json index eba758d745..5d75ae3359 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,11 @@ "dependencies": { "@babel/plugin-transform-runtime": "7.12.1", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", - "@edx/frontend-enterprise-catalog-search": "4.2.0", - "@edx/frontend-enterprise-hotjar": "1.3.0", - "@edx/frontend-enterprise-logistration": "3.2.0", - "@edx/frontend-enterprise-utils": "3.2.0", - "@edx/frontend-platform": "4.0.1", + "@edx/frontend-enterprise-catalog-search": "4.5.0", + "@edx/frontend-enterprise-hotjar": "1.4.0", + "@edx/frontend-enterprise-logistration": "3.4.0", + "@edx/frontend-enterprise-utils": "3.4.0", + "@edx/frontend-platform": "4.4.0", "@edx/paragon": "20.46.3", "@tanstack/react-query": "4.36.1", "@tanstack/react-query-devtools": "4.36.1", @@ -37,15 +37,14 @@ "lodash": "4.17.21", "lodash.debounce": "4.0.8", "prop-types": "15.7.2", - "react": "16.14.0", - "react-dom": "16.13.1", - "react-helmet": "5.2.1", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-helmet": "6.1.0", "react-instantsearch-dom": "6.8.3", "react-markdown": "6.0.0", - "react-redux": "7.1.1", + "react-redux": "7.2.9", "react-router": "5.2.0", "react-router-dom": "5.2.0", - "react-truncate": "^2.4.0", "redux": "4.0.4", "redux-devtools-extension": "2.13.8", "redux-form": "8.3.8", @@ -68,12 +67,12 @@ "@faker-js/faker": "^7.6.0", "@testing-library/dom": "9.3.1", "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "11.2.7", + "@testing-library/react": "^11.2.7", "@testing-library/react-hooks": "5.0.3", "@testing-library/user-event": "12.8.3", + "@wojtekmaj/enzyme-adapter-react-17": "0.8.0", "css-loader": "5.2.6", "enzyme": "3.11.0", - "enzyme-adapter-react-16": "1.15.6", "husky": "0.14.3", "identity-obj-proxy": "3.0.0", "jest-canvas-mock": "^2.4.0", @@ -81,7 +80,7 @@ "patch-package": "8.0.0", "postcss": "8.4.24", "react-dev-utils": "11.0.4", - "react-test-renderer": "16.13.1", + "react-test-renderer": "^17.0.2", "resize-observer-polyfill": "1.5.1", "ts-jest": "^26.5.0" }, @@ -97,35 +96,30 @@ }, "node_modules/@adobe/css-tools": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", - "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@algolia/cache-browser-local-storage": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.3.tgz", - "integrity": "sha512-Cwc03hikHSUI+xvgUdN+H+f6jFyoDsC9fegzXzJ2nPn1YSN9EXzDMBnbrgl0sbl9iLGXe0EIGMYqR2giCv1wMQ==", + "license": "MIT", "dependencies": { "@algolia/cache-common": "4.8.3" } }, "node_modules/@algolia/cache-common": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.3.tgz", - "integrity": "sha512-Cf7zZ2i6H+tLSBTkFePHhYvlgc9fnMPKsF9qTmiU38kFIGORy/TN2Fx5n1GBuRLIzaSXvcf+oHv1HvU0u1gE1g==" + "license": "MIT" }, "node_modules/@algolia/cache-in-memory": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.3.tgz", - "integrity": "sha512-+N7tkvmijXiDy2E7u1mM73AGEgGPWFmEmPeJS96oT46I98KXAwVPNYbcAqBE79YlixdXpkYJk41cFcORzNh+Iw==", + "license": "MIT", "dependencies": { "@algolia/cache-common": "4.8.3" } }, "node_modules/@algolia/client-account": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.3.tgz", - "integrity": "sha512-Uku8LqnXBwfDCtsTCDYTUOz2/2oqcAQCKgaO0uGdIR8DTQENBXFQvzziambHdn9KuFuY+6Et9k1+cjpTPBDTBg==", + "license": "MIT", "dependencies": { "@algolia/client-common": "4.8.3", "@algolia/client-search": "4.8.3", @@ -134,8 +128,7 @@ }, "node_modules/@algolia/client-analytics": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.3.tgz", - "integrity": "sha512-9ensIWmjYJprZ+YjAVSZdWUG05xEnbytENXp508X59tf34IMIX8BR2xl0RjAQODtxBdAteGxuKt5THX6U9tQLA==", + "license": "MIT", "dependencies": { "@algolia/client-common": "4.8.3", "@algolia/client-search": "4.8.3", @@ -145,8 +138,7 @@ }, "node_modules/@algolia/client-common": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.3.tgz", - "integrity": "sha512-TU3623AEFAWUQlDTznkgAMSYo8lfS9pNs5QYDQzkvzWdqK0GBDWthwdRfo9iIsfxiR9qdCMHqwEu+AlZMVhNSA==", + "license": "MIT", "dependencies": { "@algolia/requester-common": "4.8.3", "@algolia/transporter": "4.8.3" @@ -154,8 +146,7 @@ }, "node_modules/@algolia/client-recommendation": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.3.tgz", - "integrity": "sha512-qysGbmkcc6Agt29E38KWJq9JuxjGsyEYoKuX9K+P5HyQh08yR/BlRYrA8mB7vT/OIUHRGFToGO6Vq/rcg0NIOQ==", + "license": "MIT", "dependencies": { "@algolia/client-common": "4.8.3", "@algolia/requester-common": "4.8.3", @@ -164,8 +155,7 @@ }, "node_modules/@algolia/client-search": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.3.tgz", - "integrity": "sha512-rAnvoy3GAhbzOQVniFcKVn1eM2NX77LearzYNCbtFrFYavG+hJI187bNVmajToiuGZ10FfJvK99X2OB1AzzezQ==", + "license": "MIT", "dependencies": { "@algolia/client-common": "4.8.3", "@algolia/requester-common": "4.8.3", @@ -174,47 +164,40 @@ }, "node_modules/@algolia/events": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", - "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==" + "license": "MIT" }, "node_modules/@algolia/logger-common": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.3.tgz", - "integrity": "sha512-03wksHRbhl2DouEKnqWuUb64s1lV6kDAAabMCQ2Du1fb8X/WhDmxHC4UXMzypeOGlH5BZBsgVwSB7vsZLP3MZg==" + "license": "MIT" }, "node_modules/@algolia/logger-console": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.3.tgz", - "integrity": "sha512-Npt+hI4UF8t3TLMluL5utr9Gc11BjL5kDnGZOhDOAz5jYiSO2nrHMFmnpLT4Cy/u7a5t7EB5dlypuC4/AGStkA==", + "license": "MIT", "dependencies": { "@algolia/logger-common": "4.8.3" } }, "node_modules/@algolia/requester-browser-xhr": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.3.tgz", - "integrity": "sha512-/LTTIpgEmEwkyhn8yXxDdBWqXqzlgw5w2PtTpIwkSlP2/jDwdR/9w1TkFzhNbJ81ki6LAEQM5mSwoTTnbIIecg==", + "license": "MIT", "dependencies": { "@algolia/requester-common": "4.8.3" } }, "node_modules/@algolia/requester-common": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.3.tgz", - "integrity": "sha512-+Yo9vBkofoKR1SCqqtMnmnfq9yt/BiaDewY/6bYSMNxSYCnu2Fw1JKSIaf/4zos09PMSsxGpLohZwGas3+0GDQ==" + "license": "MIT" }, "node_modules/@algolia/requester-node-http": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.3.tgz", - "integrity": "sha512-k2fiKIeMIFqgC01FnzII6kqC2GQBAfbNaUX4k7QCPa6P8t4sp2xE6fImOUiztLnnL3C9X9ZX6Fw3L+cudi7jvQ==", + "license": "MIT", "dependencies": { "@algolia/requester-common": "4.8.3" } }, "node_modules/@algolia/transporter": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.3.tgz", - "integrity": "sha512-nU7fy2iU8snxATlsks0MjMyv97QJWQmOVwTjDc+KZ4+nue8CLcgm4LA4dsTBqvxeCQIoEtt3n72GwXcaqiJSjQ==", + "license": "MIT", "dependencies": { "@algolia/cache-common": "4.8.3", "@algolia/logger-common": "4.8.3", @@ -223,8 +206,7 @@ }, "node_modules/@ampproject/remapping": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -235,8 +217,7 @@ }, "node_modules/@babel/cli": { "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.21.0.tgz", - "integrity": "sha512-xi7CxyS8XjSyiwUGCfwf+brtJxjW1/ZTcBUkP10xawIEXLX5HzLn+3aXkgxozcP2UhRhtKTmQurw9Uaes7jZrA==", + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "commander": "^4.0.1", @@ -263,8 +244,7 @@ }, "node_modules/@babel/code-frame": { "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "license": "MIT", "dependencies": { "@babel/highlight": "^7.18.6" }, @@ -274,16 +254,14 @@ }, "node_modules/@babel/compat-data": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.3.tgz", - "integrity": "sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", - "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.21.4", @@ -311,8 +289,7 @@ }, "node_modules/@babel/generator": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.3.tgz", - "integrity": "sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==", + "license": "MIT", "dependencies": { "@babel/types": "^7.22.3", "@jridgewell/gen-mapping": "^0.3.2", @@ -325,8 +302,7 @@ }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -338,8 +314,7 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "license": "MIT", "dependencies": { "@babel/types": "^7.18.6" }, @@ -349,8 +324,7 @@ }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.3.tgz", - "integrity": "sha512-ahEoxgqNoYXm0k22TvOke48i1PkavGu0qGCmcq9ugi6gnmvKNaMjKBSrZTnWUi1CFEeNAUiVba0Wtzm03aSkJg==", + "license": "MIT", "dependencies": { "@babel/types": "^7.22.3" }, @@ -360,8 +334,7 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.1.tgz", - "integrity": "sha512-Rqx13UM3yVB5q0D/KwQ8+SPfX/+Rnsy1Lw1k/UwOC4KC6qrzIQoY3lYnBu5EHKBlEHHcj0M0W8ltPSkD8rqfsQ==", + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.0", "@babel/helper-validator-option": "^7.21.0", @@ -378,21 +351,18 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "license": "ISC" }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.1.tgz", - "integrity": "sha512-SowrZ9BWzYFgzUMwUmowbPSGu6CXL5MSuuCkG3bejahSpSymioPmuLdhPxNOc9MjuNGjy7M/HaXvJ8G82Lywlw==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-environment-visitor": "^7.22.1", @@ -413,8 +383,7 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.1.tgz", - "integrity": "sha512-WWjdnfR3LPIe+0EY8td7WmjhytxXtjKAEpnAxun/hkNiyOaPlvGK+NZaBFIdi9ndYV3Gav7BpFvtUwnaJlwi1w==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "regexpu-core": "^5.3.1", @@ -429,8 +398,7 @@ }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.17.7", "@babel/helper-plugin-utils": "^7.16.7", @@ -445,16 +413,14 @@ }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.1.tgz", - "integrity": "sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "license": "MIT", "dependencies": { "@babel/template": "^7.20.7", "@babel/types": "^7.21.0" @@ -465,8 +431,7 @@ }, "node_modules/@babel/helper-hoist-variables": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "license": "MIT", "dependencies": { "@babel/types": "^7.18.6" }, @@ -476,8 +441,7 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.3.tgz", - "integrity": "sha512-Gl7sK04b/2WOb6OPVeNy9eFKeD3L6++CzL3ykPOWqTn08xgYYK0wz4TUh2feIImDXxcVW3/9WQ1NMKY66/jfZA==", + "license": "MIT", "dependencies": { "@babel/types": "^7.22.3" }, @@ -487,8 +451,7 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "license": "MIT", "dependencies": { "@babel/types": "^7.21.4" }, @@ -498,8 +461,7 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.1.tgz", - "integrity": "sha512-dxAe9E7ySDGbQdCVOY/4+UcD8M9ZFqZcZhSPsPacvCG4M+9lwtDDQfI2EoaSvmf7W/8yCBkGU0m7Pvt1ru3UZw==", + "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.22.1", "@babel/helper-module-imports": "^7.21.4", @@ -516,8 +478,7 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "license": "MIT", "dependencies": { "@babel/types": "^7.18.6" }, @@ -527,16 +488,14 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", - "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-environment-visitor": "^7.18.9", @@ -552,8 +511,7 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.1.tgz", - "integrity": "sha512-ut4qrkE4AuSfrwHSps51ekR1ZY/ygrP1tp0WFm8oVq6nzc/hvfV/22JylndIbsf2U2M9LOMwiSddr6y+78j+OQ==", + "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.22.1", "@babel/helper-member-expression-to-functions": "^7.22.0", @@ -568,8 +526,7 @@ }, "node_modules/@babel/helper-simple-access": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", - "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "license": "MIT", "dependencies": { "@babel/types": "^7.21.5" }, @@ -579,8 +536,7 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "license": "MIT", "dependencies": { "@babel/types": "^7.20.0" }, @@ -590,8 +546,7 @@ }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "license": "MIT", "dependencies": { "@babel/types": "^7.18.6" }, @@ -601,32 +556,28 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "license": "MIT", "dependencies": { "@babel/helper-function-name": "^7.19.0", "@babel/template": "^7.18.10", @@ -639,8 +590,7 @@ }, "node_modules/@babel/helpers": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.3.tgz", - "integrity": "sha512-jBJ7jWblbgr7r6wYZHMdIqKc73ycaTcCaWRq4/2LpuPHcx7xMlZvpGQkOYc9HeSjn6rcx15CPlgVcBtZ4WZJ2w==", + "license": "MIT", "dependencies": { "@babel/template": "^7.21.9", "@babel/traverse": "^7.22.1", @@ -652,8 +602,7 @@ }, "node_modules/@babel/highlight": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -665,8 +614,7 @@ }, "node_modules/@babel/parser": { "version": "7.22.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz", - "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==", + "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -676,8 +624,7 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -690,8 +637,7 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.3.tgz", - "integrity": "sha512-6r4yRwEnorYByILoDRnEqxtojYKuiIv9FojW2E8GUKo9eWBwbKcd9IiZOZpdyXc64RmyGGyPu3/uAcrz/dq2kQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", @@ -706,8 +652,7 @@ }, "node_modules/@babel/plugin-proposal-async-generator-functions": { "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-plugin-utils": "^7.20.2", @@ -723,8 +668,7 @@ }, "node_modules/@babel/plugin-proposal-class-properties": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -738,8 +682,7 @@ }, "node_modules/@babel/plugin-proposal-class-static-block": { "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.21.0", "@babel/helper-plugin-utils": "^7.20.2", @@ -754,8 +697,7 @@ }, "node_modules/@babel/plugin-proposal-dynamic-import": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -769,8 +711,7 @@ }, "node_modules/@babel/plugin-proposal-export-namespace-from": { "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.9", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -784,8 +725,7 @@ }, "node_modules/@babel/plugin-proposal-json-strings": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-json-strings": "^7.8.3" @@ -799,8 +739,7 @@ }, "node_modules/@babel/plugin-proposal-logical-assignment-operators": { "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -814,8 +753,7 @@ }, "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -829,8 +767,7 @@ }, "node_modules/@babel/plugin-proposal-numeric-separator": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -844,8 +781,7 @@ }, "node_modules/@babel/plugin-proposal-object-rest-spread": { "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.20.5", "@babel/helper-compilation-targets": "^7.20.7", @@ -862,8 +798,7 @@ }, "node_modules/@babel/plugin-proposal-optional-catch-binding": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" @@ -877,8 +812,7 @@ }, "node_modules/@babel/plugin-proposal-optional-chaining": { "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", @@ -893,8 +827,7 @@ }, "node_modules/@babel/plugin-proposal-private-methods": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -908,8 +841,7 @@ }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", - "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-create-class-features-plugin": "^7.21.0", @@ -925,8 +857,7 @@ }, "node_modules/@babel/plugin-proposal-unicode-property-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -940,8 +871,7 @@ }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -951,8 +881,7 @@ }, "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -962,8 +891,7 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -973,8 +901,7 @@ }, "node_modules/@babel/plugin-syntax-class-static-block": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -987,8 +914,7 @@ }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -998,8 +924,7 @@ }, "node_modules/@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, @@ -1009,8 +934,7 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.19.0" }, @@ -1023,8 +947,7 @@ }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1034,8 +957,7 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1045,8 +967,7 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", - "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2" }, @@ -1059,8 +980,7 @@ }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1070,8 +990,7 @@ }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1081,8 +1000,7 @@ }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1092,8 +1010,7 @@ }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1103,8 +1020,7 @@ }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1114,8 +1030,7 @@ }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1125,8 +1040,7 @@ }, "node_modules/@babel/plugin-syntax-private-property-in-object": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1139,8 +1053,7 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1153,8 +1066,7 @@ }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", - "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2" }, @@ -1167,8 +1079,7 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz", - "integrity": "sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5" }, @@ -1181,8 +1092,7 @@ }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.18.6", "@babel/helper-plugin-utils": "^7.20.2", @@ -1197,8 +1107,7 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1211,8 +1120,7 @@ }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2" }, @@ -1225,8 +1133,7 @@ }, "node_modules/@babel/plugin-transform-classes": { "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-compilation-targets": "^7.20.7", @@ -1247,8 +1154,7 @@ }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz", - "integrity": "sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5", "@babel/template": "^7.20.7" @@ -1262,8 +1168,7 @@ }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2" }, @@ -1276,8 +1181,7 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -1291,8 +1195,7 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.9" }, @@ -1305,8 +1208,7 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "license": "MIT", "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -1320,8 +1222,7 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz", - "integrity": "sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5" }, @@ -1334,8 +1235,7 @@ }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.18.9", "@babel/helper-function-name": "^7.18.9", @@ -1350,8 +1250,7 @@ }, "node_modules/@babel/plugin-transform-literals": { "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.9" }, @@ -1364,8 +1263,7 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1378,8 +1276,7 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.20.11", "@babel/helper-plugin-utils": "^7.20.2" @@ -1393,8 +1290,7 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz", - "integrity": "sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==", + "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.21.5", "@babel/helper-plugin-utils": "^7.21.5", @@ -1409,8 +1305,7 @@ }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.3.tgz", - "integrity": "sha512-V21W3bKLxO3ZjcBJZ8biSvo5gQ85uIXW2vJfh7JSWf/4SLUSr1tOoHX3ruN4+Oqa2m+BKfsxTR1I+PsvkIWvNw==", + "license": "MIT", "dependencies": { "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-module-transforms": "^7.22.1", @@ -1426,8 +1321,7 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -1441,8 +1335,7 @@ }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.3.tgz", - "integrity": "sha512-c6HrD/LpUdNNJsISQZpds3TXvfYIAbo+efE9aWmY/PmSRD0agrJ9cPMt4BmArwUQ7ZymEWTFjTyp+yReLJZh0Q==", + "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.1", "@babel/helper-plugin-utils": "^7.21.5" @@ -1456,8 +1349,7 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.3.tgz", - "integrity": "sha512-5RuJdSo89wKdkRTqtM9RVVJzHum9c2s0te9rB7vZC1zKKxcioWIy+xcu4OoIAjyFZhb/bp5KkunuLin1q7Ct+w==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5" }, @@ -1470,8 +1362,7 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/helper-replace-supers": "^7.18.6" @@ -1485,8 +1376,7 @@ }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.3.tgz", - "integrity": "sha512-63v3/UFFxhPKT8j8u1jTTGVyITxl7/7AfOqK8C5gz1rHURPUGe3y5mvIf68eYKGoBNahtJnTxBKug4BQOnzeJg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", @@ -1501,8 +1391,7 @@ }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.3.tgz", - "integrity": "sha512-x7QHQJHPuD9VmfpzboyGJ5aHEr9r7DsAsdxdhJiTB3J3j8dyl+NFZ+rX5Q2RWFDCs61c06qBfS4ys2QYn8UkMw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5" }, @@ -1515,8 +1404,7 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1529,8 +1417,7 @@ }, "node_modules/@babel/plugin-transform-react-constant-elements": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.3.tgz", - "integrity": "sha512-b5J6muxQYp4H7loAQv/c7GO5cPuRA6H5hx4gO+/Hn+Cu9MRQU0PNiUoWq1L//8sq6kFSNxGXFb2XTaUfa9y+Pg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5" }, @@ -1543,8 +1430,7 @@ }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", - "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1557,8 +1443,7 @@ }, "node_modules/@babel/plugin-transform-react-jsx": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.3.tgz", - "integrity": "sha512-JEulRWG2f04a7L8VWaOngWiK6p+JOSpB+DAtwfJgOaej1qdbNxqtK7MwTBHjUA10NeFcszlFNqCdbRcirzh2uQ==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-module-imports": "^7.21.4", @@ -1575,8 +1460,7 @@ }, "node_modules/@babel/plugin-transform-react-jsx-development": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", - "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "license": "MIT", "dependencies": { "@babel/plugin-transform-react-jsx": "^7.18.6" }, @@ -1589,8 +1473,7 @@ }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", - "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -1604,8 +1487,7 @@ }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", - "integrity": "sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5", "regenerator-transform": "^0.15.1" @@ -1619,8 +1501,7 @@ }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1633,8 +1514,7 @@ }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.1.tgz", - "integrity": "sha512-Ac/H6G9FEIkS2tXsZjL4RAdS3L3WHxci0usAnz7laPWUmFiGtj7tIASChqKZMHTSQTQY6xDbOq+V1/vIq3QrWg==", + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4", @@ -1647,16 +1527,14 @@ }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1669,8 +1547,7 @@ }, "node_modules/@babel/plugin-transform-spread": { "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" @@ -1684,8 +1561,7 @@ }, "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -1698,8 +1574,7 @@ }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.9" }, @@ -1712,8 +1587,7 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.9" }, @@ -1726,8 +1600,7 @@ }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.3.tgz", - "integrity": "sha512-pyjnCIniO5PNaEuGxT28h0HbMru3qCVrMqVgVOz/krComdIrY9W6FCLBq9NWHY8HDGaUlan+UhmZElDENIfCcw==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-create-class-features-plugin": "^7.22.1", @@ -1743,8 +1616,7 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz", - "integrity": "sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5" }, @@ -1757,8 +1629,7 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -1772,8 +1643,7 @@ }, "node_modules/@babel/preset-env": { "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", - "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.21.4", "@babel/helper-compilation-targets": "^7.21.4", @@ -1860,8 +1730,7 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", @@ -1875,8 +1744,7 @@ }, "node_modules/@babel/preset-react": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", - "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/helper-validator-option": "^7.18.6", @@ -1894,8 +1762,7 @@ }, "node_modules/@babel/preset-typescript": { "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz", - "integrity": "sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==", + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.21.5", "@babel/helper-validator-option": "^7.21.0", @@ -1912,13 +1779,11 @@ }, "node_modules/@babel/regjsgen": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + "license": "MIT" }, "node_modules/@babel/runtime": { "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", - "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -1927,9 +1792,9 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.22.5.tgz", - "integrity": "sha512-TNPDN6aBFaUox2Lu+H/Y1dKKQgr4ucz/FGyCz67RVYLsBpVpUFf1dDngzg+Od8aqbrqwyztkaZjtWCZEUOT8zA==", + "version": "7.22.6", + "dev": true, + "license": "MIT", "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.13.11" @@ -1940,18 +1805,16 @@ }, "node_modules/@babel/runtime-corejs3/node_modules/regenerator-runtime": { "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "dev": true, + "license": "MIT" }, "node_modules/@babel/runtime/node_modules/regenerator-runtime": { "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "license": "MIT" }, "node_modules/@babel/template": { "version": "7.21.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.21.9.tgz", - "integrity": "sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.21.4", "@babel/parser": "^7.21.9", @@ -1963,8 +1826,7 @@ }, "node_modules/@babel/traverse": { "version": "7.22.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.4.tgz", - "integrity": "sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.21.4", "@babel/generator": "^7.22.3", @@ -1983,8 +1845,7 @@ }, "node_modules/@babel/types": { "version": "7.22.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.4.tgz", - "integrity": "sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.21.5", "@babel/helper-validator-identifier": "^7.19.1", @@ -1996,13 +1857,11 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + "license": "MIT" }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "license": "Apache-2.0", "dependencies": { "exec-sh": "^0.3.2", "minimist": "^1.2.0" @@ -2016,16 +1875,14 @@ }, "node_modules/@cospired/i18n-iso-languages": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-2.2.0.tgz", - "integrity": "sha512-hywY9u9apWGeLxQuRcXw7IW0XkMdXum/hr3TpmHY2fAbXMTFlhhkPCdsQeHzjxMQwTnMgXaZ4j4WOCwKtlDRCQ==", + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.2.tgz", - "integrity": "sha512-xm7Mgwej/wBfLoK0K5LfntmPJzoULayl1XZY9JYgQgT29JiqNw++sLnx95u5y9zCihblzkyaRYJrsRMhIBzRdg==", + "license": "MIT", "engines": { "node": "^14 || ^16 || >=18" }, @@ -2040,8 +1897,6 @@ }, "node_modules/@csstools/css-parser-algorithms": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.2.0.tgz", - "integrity": "sha512-9BoQ/jSrPq4vv3b9jjLW+PNNv56KlDH5JMx5yASSNrCtvq70FCNZUjXRvbCeR9hYj9ZyhURtqpU/RFIgg6kiOw==", "funding": [ { "type": "github", @@ -2052,6 +1907,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": "^14 || ^16 || >=18" }, @@ -2061,8 +1917,7 @@ }, "node_modules/@csstools/css-tokenizer": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz", - "integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==", + "license": "MIT", "engines": { "node": "^14 || ^16 || >=18" }, @@ -2073,8 +1928,6 @@ }, "node_modules/@csstools/media-query-list-parser": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.0.tgz", - "integrity": "sha512-MXkR+TeaS2q9IkpyO6jVCdtA/bfpABJxIrfkLswThFN8EZZgI2RfAHhm6sDNDuYV25d5+b8Lj1fpTccIcSLPsQ==", "funding": [ { "type": "github", @@ -2085,6 +1938,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": "^14 || ^16 || >=18" }, @@ -2095,28 +1949,25 @@ }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/@edx/brand": { "name": "@openedx/brand-openedx", - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@openedx/brand-openedx/-/brand-openedx-1.2.2.tgz", - "integrity": "sha512-mBvxR7aB9290j9+h3d/9G8VkG1b8ecLSmlxc0vskfm7DL/fKUzFmHAj3PI7Z4kkwCQOL4QT5mJHJKC0ZFf7qvQ==" + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@openedx/brand-openedx/-/brand-openedx-1.2.3.tgz", + "integrity": "sha512-Dn9CtpC8fovh++Xi4NF5NJoeR9yU2yXZnV9IujxIyGd/dn0Phq5t6dzJVfupwq09mpDnzJv7egA8Znz/3ljO+w==" }, "node_modules/@edx/browserslist-config": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@edx/browserslist-config/-/browserslist-config-1.0.0.tgz", - "integrity": "sha512-gLAlpz9Y5VruxqiUBTROG7PvouIxoMc6dvhvNpXUDHRN0KEke+zBj+zJ4frL9kGbkeex273nzSazbG42hNDLrg==", - "dev": true + "dev": true, + "license": "AGPL-3.0" }, "node_modules/@edx/eslint-config": { "version": "4.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@edx/eslint-config/-/eslint-config-4.0.0-alpha.1.tgz", - "integrity": "sha512-+Hx8r6z+DdwryqluA0MF7S/RCurakzQs4AfNufWu2BLcIaf/Jot19NfcxS4Dzo8WUWiBL+lo4/42fclHHrvk9Q==", + "license": "MIT", "peerDependencies": { "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", @@ -2131,8 +1982,7 @@ }, "node_modules/@edx/frontend-build": { "version": "12.9.0-alpha.1", - "resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-12.9.0-alpha.1.tgz", - "integrity": "sha512-boHfp7yzUn5JptWGi2jK/EMaIlJ4nzjZBp7PW19vkNokbuQ3ansJsRvMscFg83tw0lXRTKCPN1y0uKg92AMizg==", + "license": "AGPL-3.0", "dependencies": { "@babel/cli": "7.21.0", "@babel/core": "7.21.4", @@ -2204,13 +2054,11 @@ }, "node_modules/@edx/frontend-build/node_modules/@types/html-minifier-terser": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" + "license": "MIT" }, "node_modules/@edx/frontend-build/node_modules/@types/jest": { "version": "26.0.24", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", - "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "license": "MIT", "dependencies": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" @@ -2218,8 +2066,7 @@ }, "node_modules/@edx/frontend-build/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -2232,16 +2079,14 @@ }, "node_modules/@edx/frontend-build/node_modules/array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@edx/frontend-build/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2255,8 +2100,7 @@ }, "node_modules/@edx/frontend-build/node_modules/clean-css": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz", - "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==", + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -2266,16 +2110,14 @@ }, "node_modules/@edx/frontend-build/node_modules/clean-css/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@edx/frontend-build/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2285,21 +2127,18 @@ }, "node_modules/@edx/frontend-build/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@edx/frontend-build/node_modules/commander": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/@edx/frontend-build/node_modules/cosmiconfig": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.1.0", @@ -2313,8 +2152,7 @@ }, "node_modules/@edx/frontend-build/node_modules/css-loader": { "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "loader-utils": "^2.0.0", @@ -2340,8 +2178,7 @@ }, "node_modules/@edx/frontend-build/node_modules/css-loader/node_modules/semver": { "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2354,8 +2191,7 @@ }, "node_modules/@edx/frontend-build/node_modules/css-select": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -2369,8 +2205,7 @@ }, "node_modules/@edx/frontend-build/node_modules/cssnano": { "version": "5.1.15", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", - "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "license": "MIT", "dependencies": { "cssnano-preset-default": "^5.2.14", "lilconfig": "^2.0.3", @@ -2389,8 +2224,7 @@ }, "node_modules/@edx/frontend-build/node_modules/cssnano-preset-default": { "version": "5.2.14", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", - "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "license": "MIT", "dependencies": { "css-declaration-sorter": "^6.3.1", "cssnano-utils": "^3.1.0", @@ -2431,8 +2265,7 @@ }, "node_modules/@edx/frontend-build/node_modules/cssnano-utils": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "license": "MIT", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -2442,16 +2275,14 @@ }, "node_modules/@edx/frontend-build/node_modules/diff-sequences": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/@edx/frontend-build/node_modules/dom-serializer": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -2463,8 +2294,7 @@ }, "node_modules/@edx/frontend-build/node_modules/domhandler": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -2477,8 +2307,7 @@ }, "node_modules/@edx/frontend-build/node_modules/domutils": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -2490,16 +2319,14 @@ }, "node_modules/@edx/frontend-build/node_modules/dotenv": { "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "license": "BSD-2-Clause", "engines": { "node": ">=10" } }, "node_modules/@edx/frontend-build/node_modules/dotenv-webpack": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-7.1.1.tgz", - "integrity": "sha512-xw/19VqHDkXALtBOJAnnrSU/AZDIQRXczAmJyp0lZv6SH2aBLzUTl96W1MVryJZ7okZ+djZS4Gj4KlZ0xP7deA==", + "license": "MIT", "dependencies": { "dotenv-defaults": "^2.0.2" }, @@ -2512,16 +2339,14 @@ }, "node_modules/@edx/frontend-build/node_modules/entities": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/@edx/frontend-build/node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -2531,16 +2356,14 @@ }, "node_modules/@edx/frontend-build/node_modules/filesize": { "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "license": "BSD-3-Clause", "engines": { "node": ">= 0.4.0" } }, "node_modules/@edx/frontend-build/node_modules/globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -2558,8 +2381,7 @@ }, "node_modules/@edx/frontend-build/node_modules/gzip-size": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", "dependencies": { "duplexer": "^0.1.2" }, @@ -2572,16 +2394,14 @@ }, "node_modules/@edx/frontend-build/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@edx/frontend-build/node_modules/html-minifier-terser": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", @@ -2600,8 +2420,7 @@ }, "node_modules/@edx/frontend-build/node_modules/html-webpack-plugin": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -2622,8 +2441,6 @@ }, "node_modules/@edx/frontend-build/node_modules/htmlparser2": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -2631,6 +2448,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -2640,8 +2458,7 @@ }, "node_modules/@edx/frontend-build/node_modules/immer": { "version": "9.0.19", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz", - "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -2649,8 +2466,7 @@ }, "node_modules/@edx/frontend-build/node_modules/jest-diff": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -2663,16 +2479,14 @@ }, "node_modules/@edx/frontend-build/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/@edx/frontend-build/node_modules/mini-css-extract-plugin": { "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0", @@ -2691,8 +2505,7 @@ }, "node_modules/@edx/frontend-build/node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -2702,8 +2515,7 @@ }, "node_modules/@edx/frontend-build/node_modules/open": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -2718,8 +2530,6 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss": { "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "funding": [ { "type": "opencollective", @@ -2730,6 +2540,7 @@ "url": "https://tidelift.com/funding/github/npm/postcss" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", @@ -2741,8 +2552,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-calc": { "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" @@ -2753,8 +2563,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-colormin": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", - "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0", @@ -2770,8 +2579,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-convert-values": { "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", - "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.4", "postcss-value-parser": "^4.2.0" @@ -2785,8 +2593,6 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-custom-media": { "version": "9.1.4", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-9.1.4.tgz", - "integrity": "sha512-4A7WEG3iIyKwfpxL5bkuSlHoHHGRTHl0212Z3uvpwJPyVfZJlkZAQNNgVC+oogrJgksDnfKyuuMbG6HafZPW8Q==", "funding": [ { "type": "github", @@ -2797,6 +2603,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { "@csstools/cascade-layer-name-parser": "^1.0.2", "@csstools/css-parser-algorithms": "^2.1.1", @@ -2812,8 +2619,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-discard-comments": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "license": "MIT", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -2823,8 +2629,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-discard-duplicates": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "license": "MIT", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -2834,8 +2639,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-discard-empty": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "license": "MIT", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -2845,8 +2649,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-discard-overridden": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "license": "MIT", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -2856,8 +2659,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-merge-longhand": { "version": "5.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", - "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^5.1.1" @@ -2871,8 +2673,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-merge-rules": { "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", - "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0", @@ -2888,8 +2689,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-minify-font-values": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -2902,8 +2702,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-minify-gradients": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "license": "MIT", "dependencies": { "colord": "^2.9.1", "cssnano-utils": "^3.1.0", @@ -2918,8 +2717,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-minify-params": { "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", - "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.4", "cssnano-utils": "^3.1.0", @@ -2934,8 +2732,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-minify-selectors": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", - "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.5" }, @@ -2948,8 +2745,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-charset": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "license": "MIT", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -2959,8 +2755,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-display-values": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -2973,8 +2768,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-positions": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", - "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -2987,8 +2781,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-repeat-style": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", - "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -3001,8 +2794,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-string": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -3015,8 +2807,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-timing-functions": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -3029,8 +2820,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-unicode": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", - "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.4", "postcss-value-parser": "^4.2.0" @@ -3044,8 +2834,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-url": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "license": "MIT", "dependencies": { "normalize-url": "^6.0.1", "postcss-value-parser": "^4.2.0" @@ -3059,8 +2848,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-normalize-whitespace": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -3073,8 +2861,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-ordered-values": { "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", - "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "license": "MIT", "dependencies": { "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" @@ -3088,8 +2875,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-reduce-initial": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", - "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0" @@ -3103,8 +2889,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-reduce-transforms": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -3117,8 +2902,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-svgo": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^2.7.0" @@ -3132,8 +2916,7 @@ }, "node_modules/@edx/frontend-build/node_modules/postcss-unique-selectors": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.5" }, @@ -3146,8 +2929,7 @@ }, "node_modules/@edx/frontend-build/node_modules/pretty-error": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" @@ -3155,8 +2937,7 @@ }, "node_modules/@edx/frontend-build/node_modules/react-dev-utils": { "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.16.0", "address": "^1.1.2", @@ -3189,8 +2970,7 @@ }, "node_modules/@edx/frontend-build/node_modules/react-dev-utils/node_modules/fork-ts-checker-webpack-plugin": { "version": "6.5.2", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", - "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.8.3", "@types/json-schema": "^7.0.5", @@ -3227,16 +3007,14 @@ }, "node_modules/@edx/frontend-build/node_modules/react-dev-utils/node_modules/loader-utils": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "license": "MIT", "engines": { "node": ">= 12.13.0" } }, "node_modules/@edx/frontend-build/node_modules/react-dev-utils/node_modules/schema-utils": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.4", "ajv": "^6.12.2", @@ -3252,8 +3030,7 @@ }, "node_modules/@edx/frontend-build/node_modules/react-dev-utils/node_modules/semver": { "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -3266,16 +3043,14 @@ }, "node_modules/@edx/frontend-build/node_modules/react-dev-utils/node_modules/tapable": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@edx/frontend-build/node_modules/renderkid": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", @@ -3286,24 +3061,21 @@ }, "node_modules/@edx/frontend-build/node_modules/shell-quote": { "version": "1.7.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz", - "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/@edx/frontend-build/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@edx/frontend-build/node_modules/stylehacks": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", - "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.4", "postcss-selector-parser": "^6.0.4" @@ -3317,8 +3089,7 @@ }, "node_modules/@edx/frontend-build/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3328,16 +3099,14 @@ }, "node_modules/@edx/frontend-build/node_modules/tapable": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@edx/frontend-build/node_modules/terser": { "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", @@ -3353,13 +3122,11 @@ }, "node_modules/@edx/frontend-build/node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "license": "MIT" }, "node_modules/@edx/frontend-build/node_modules/webpack-merge": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "wildcard": "^2.0.0" @@ -3369,11 +3136,10 @@ } }, "node_modules/@edx/frontend-enterprise-catalog-search": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-enterprise-catalog-search/-/frontend-enterprise-catalog-search-4.2.0.tgz", - "integrity": "sha512-rg/kix5IcQZFNaV7v4qCyZQJYm7ilXyjBIbnszY9GaK75crAKcezoRYruS36TNeYLuUo2ri8NwrZ73OxFuInwg==", + "version": "4.5.0", + "license": "AGPL-3.0", "dependencies": { - "@edx/frontend-enterprise-utils": "^3.2.0", + "@edx/frontend-enterprise-utils": "^3.4.0", "classnames": "2.2.5", "lodash.debounce": "4.0.8", "prop-types": "15.7.2" @@ -3383,100 +3149,89 @@ "@edx/paragon": "^19.15.0 || ^20.0.0", "@fortawesome/free-solid-svg-icons": "^5.8.1", "@fortawesome/react-fontawesome": "^0.1.4", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.12.0 || ^17.0.0", + "react-dom": "^16.12.0 || ^17.0.0", "react-instantsearch-dom": "^6.8.3", "react-router-dom": "^5.2.0" } }, "node_modules/@edx/frontend-enterprise-catalog-search/node_modules/classnames": { "version": "2.2.5", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", - "integrity": "sha512-DTt3GhOUDKhh4ONwIJW4lmhyotQmV2LjNlGK/J2hkwUcqcbKkCLAdJPtxQnxnlc7SR3f1CEXCyMmc7WLUsWbNA==" + "license": "MIT" }, "node_modules/@edx/frontend-enterprise-hotjar": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-enterprise-hotjar/-/frontend-enterprise-hotjar-1.3.0.tgz", - "integrity": "sha512-50xawVceXsj7Olr5u2UnmBOnyOLj8WHRdr2Y9/vR21U/Kd3Qa/bj8eSx8p4HqSlFbVMcKwfgszcBZx76nmaldw==", + "version": "1.4.0", + "license": "AGPL-3.0", "peerDependencies": { - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.12.0 || ^17.0.0", + "react-dom": "^16.12.0 || ^17.0.0", "react-router-dom": "^5.2.0" } }, "node_modules/@edx/frontend-enterprise-logistration": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-enterprise-logistration/-/frontend-enterprise-logistration-3.2.0.tgz", - "integrity": "sha512-iM7EceyAMGWnCQTVIzRhMxVHfSY/KdkiX7Whb1e7SbL/e7dB2MtP/MYiAfmk2qDGubzDcfdHVaxevQxdP6l5pg==", + "version": "3.4.0", + "license": "AGPL-3.0", "dependencies": { - "@edx/frontend-enterprise-utils": "^3.2.0", + "@edx/frontend-enterprise-utils": "^3.4.0", "prop-types": "15.7.2" }, "peerDependencies": { "@edx/frontend-platform": "^4.0.1", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.12.0 || ^17.0.0", + "react-dom": "^16.12.0 || ^17.0.0", "react-router-dom": "^5.2.0" } }, "node_modules/@edx/frontend-enterprise-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-enterprise-utils/-/frontend-enterprise-utils-3.2.0.tgz", - "integrity": "sha512-3lXRu2QTiOaIF4wljNLjeoUSRFseDnMbw/+/RmK1dAplqvoi632fX9y0lMm0q8Nnr8FP9XyxxAoOYeDQwGIQ+w==", + "version": "3.4.0", + "license": "AGPL-3.0", "dependencies": { - "@testing-library/react": "11.2.6", + "@testing-library/react": "12.1.4", "history": "4.10.1" }, "peerDependencies": { "@edx/frontend-platform": "^4.0.1", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.12.0 || ^17.0.0", + "react-dom": "^16.12.0 || ^17.0.0", "react-router-dom": "^5.2.0" } }, "node_modules/@edx/frontend-enterprise-utils/node_modules/@testing-library/dom": { - "version": "7.31.2", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", - "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", + "version": "8.20.1", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^4.2.2", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.6", - "lz-string": "^1.4.4", - "pretty-format": "^26.6.2" + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/@edx/frontend-enterprise-utils/node_modules/@testing-library/react": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz", - "integrity": "sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ==", + "version": "12.1.4", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^7.28.1" + "@testing-library/dom": "^8.0.0", + "@types/react-dom": "*" }, "engines": { - "node": ">=10" + "node": ">=12" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, - "node_modules/@edx/frontend-enterprise-utils/node_modules/@types/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==" - }, "node_modules/@edx/frontend-enterprise-utils/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -3487,22 +3242,9 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@edx/frontend-enterprise-utils/node_modules/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "dependencies": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - }, - "engines": { - "node": ">=6.0" - } - }, "node_modules/@edx/frontend-enterprise-utils/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3516,8 +3258,7 @@ }, "node_modules/@edx/frontend-enterprise-utils/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -3527,21 +3268,44 @@ }, "node_modules/@edx/frontend-enterprise-utils/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@edx/frontend-enterprise-utils/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/@edx/frontend-enterprise-utils/node_modules/pretty-format": { + "version": "27.5.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@edx/frontend-enterprise-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@edx/frontend-enterprise-utils/node_modules/react-is": { + "version": "17.0.2", + "license": "MIT" + }, "node_modules/@edx/frontend-enterprise-utils/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3550,9 +3314,8 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.0.1.tgz", - "integrity": "sha512-79uo/iQJ7P1tIY0MHTQ874W/NrNkcDe4BFC/0AvKF4DNrkqq9AkUFKqQ3hvd8E9C1EK189KlcAmo6FFQ67IFXg==", + "version": "4.4.0", + "license": "AGPL-3.0", "dependencies": { "@cospired/i18n-iso-languages": "2.2.0", "@formatjs/intl-pluralrules": "4.3.3", @@ -3575,13 +3338,14 @@ "universal-cookie": "4.0.4" }, "bin": { + "intl-imports.js": "i18n/scripts/intl-imports.js", "transifex-utils.js": "i18n/scripts/transifex-utils.js" }, "peerDependencies": { "@edx/paragon": ">= 10.0.0 < 21.0.0", "prop-types": "^15.7.2", - "react": "^16.9.0", - "react-dom": "^16.9.0", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0", "react-redux": "^7.1.1", "react-router-dom": "^5.0.1", "redux": "^4.0.4" @@ -3589,16 +3353,21 @@ }, "node_modules/@edx/new-relic-source-map-webpack-plugin": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@edx/new-relic-source-map-webpack-plugin/-/new-relic-source-map-webpack-plugin-1.0.2.tgz", - "integrity": "sha512-jwu9WjRtEbv0rvPHGCnhbQbbv6+DTADShj43NQVsuAyD823znjutgoHnlc+9HIOiYiIxjz/wIMcGVwjrTnMceQ==", + "license": "AGPL-3.0", "dependencies": { "@newrelic/publish-sourcemap": "^5.0.1" } }, "node_modules/@edx/paragon": { "version": "20.46.3", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.3.tgz", - "integrity": "sha512-cHxoxoOREVFbBqW9IRAtlIAQo1lcF9JJXkLoEw1Vam6oetKSa5Mc0SL5kykbV+1iRPP7kS8A0Csf5nRr0oolLQ==", + "license": "Apache-2.0", + "workspaces": [ + "example", + "component-generator", + "www", + "icons", + "dependent-usage-analyzer" + ], "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", @@ -3634,18 +3403,16 @@ }, "node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-common-types": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz", - "integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==", "hasInstallScript": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-svg-core": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.1.tgz", - "integrity": "sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@fortawesome/fontawesome-common-types": "6.2.1" }, @@ -3655,8 +3422,7 @@ }, "node_modules/@edx/paragon/node_modules/@fortawesome/react-fontawesome": { "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", - "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", + "license": "MIT", "dependencies": { "prop-types": "^15.8.1" }, @@ -3667,21 +3433,18 @@ }, "node_modules/@edx/paragon/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@edx/paragon/node_modules/classnames": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + "license": "MIT" }, "node_modules/@edx/paragon/node_modules/glob": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3698,8 +3461,7 @@ }, "node_modules/@edx/paragon/node_modules/minimatch": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3709,8 +3471,7 @@ }, "node_modules/@edx/paragon/node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3719,16 +3480,14 @@ }, "node_modules/@edx/typescript-config": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@edx/typescript-config/-/typescript-config-1.0.1.tgz", - "integrity": "sha512-w0g3nIX9oEch8Rip8q8sb/nrurGEHA1BEjK/I1LAQwA44K4FPMWvyvabmZErrdTJ9sXcZL10aWD3bat1obV8Bg==", + "license": "MIT", "peerDependencies": { "typescript": "^4.9.4" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -3741,16 +3500,14 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -3771,13 +3528,11 @@ }, "node_modules/@eslint/eslintrc/node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -3790,8 +3545,7 @@ }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3801,8 +3555,7 @@ }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -3812,17 +3565,15 @@ }, "node_modules/@eslint/js": { "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@faker-js/faker": { "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0", "npm": ">=6.0.0" @@ -3830,8 +3581,7 @@ }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", - "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "license": "MIT", "dependencies": { "@formatjs/intl-localematcher": "0.2.25", "tslib": "^2.1.0" @@ -3839,16 +3589,14 @@ }, "node_modules/@formatjs/fast-memoize": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", - "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", + "license": "MIT", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@formatjs/icu-messageformat-parser": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", - "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/icu-skeleton-parser": "1.3.6", @@ -3857,8 +3605,7 @@ }, "node_modules/@formatjs/icu-skeleton-parser": { "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", - "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "tslib": "^2.1.0" @@ -3866,8 +3613,7 @@ }, "node_modules/@formatjs/intl": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.2.1.tgz", - "integrity": "sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/fast-memoize": "1.2.1", @@ -3888,8 +3634,7 @@ }, "node_modules/@formatjs/intl-displaynames": { "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-5.4.3.tgz", - "integrity": "sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/intl-localematcher": "0.2.25", @@ -3898,8 +3643,7 @@ }, "node_modules/@formatjs/intl-listformat": { "version": "6.5.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-6.5.3.tgz", - "integrity": "sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/intl-localematcher": "0.2.25", @@ -3908,16 +3652,14 @@ }, "node_modules/@formatjs/intl-localematcher": { "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", - "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "license": "MIT", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@formatjs/intl-numberformat": { "version": "5.7.6", - "resolved": "https://registry.npmjs.org/@formatjs/intl-numberformat/-/intl-numberformat-5.7.6.tgz", - "integrity": "sha512-ZlZfYtvbVHYZY5OG3RXizoCwxKxEKOrzEe2YOw9wbzoxF3PmFn0SAgojCFGLyNXkkR6xVxlylhbuOPf1dkIVNg==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.4.0", "tslib": "^2.0.1" @@ -3925,16 +3667,14 @@ }, "node_modules/@formatjs/intl-numberformat/node_modules/@formatjs/ecma402-abstract": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.4.0.tgz", - "integrity": "sha512-Mv027hcLFjE45K8UJ8PjRpdDGfR0aManEFj1KzoN8zXNveHGEygpZGfFf/FTTMl+QEVSrPAUlyxaCApvmv47AQ==", + "license": "MIT", "dependencies": { "tslib": "^2.0.1" } }, "node_modules/@formatjs/intl-pluralrules": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.3.3.tgz", - "integrity": "sha512-NLZN8gf2qLpCuc0m565IbKLNUarEGOzk0mkdTkE4XTuNCofzoQTurW6lL3fmDlneAoYl2FiTdHa5q4o2vZF50g==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/intl-localematcher": "0.2.25", @@ -3943,8 +3683,7 @@ }, "node_modules/@formatjs/intl-relativetimeformat": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-10.0.1.tgz", - "integrity": "sha512-AABPQtPjFilXegQsnmVHrSlzjFNUffAEk5DgowY6b7WSwDI7g2W6QgW903/lbZ58emhphAbgHdtKeUBXqTiLpw==", + "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/intl-localematcher": "0.2.25", @@ -3953,8 +3692,7 @@ }, "node_modules/@formatjs/ts-transformer": { "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-2.13.0.tgz", - "integrity": "sha512-mu7sHXZk1NWZrQ3eUqugpSYo8x5/tXkrI4uIbFqCEC0eNgQaIcoKgVeDFgDAcgG+cEme2atAUYSFF+DFWC4org==", + "license": "MIT", "dependencies": { "intl-messageformat-parser": "6.1.2", "tslib": "^2.0.1", @@ -3971,17 +3709,14 @@ }, "node_modules/@formatjs/ts-transformer/node_modules/@formatjs/ecma402-abstract": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz", - "integrity": "sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q==", + "license": "MIT", "dependencies": { "tslib": "^2.0.1" } }, "node_modules/@formatjs/ts-transformer/node_modules/intl-messageformat-parser": { "version": "6.1.2", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.2.tgz", - "integrity": "sha512-4GQDEPhl/ZMNDKwMsLqyw1LG2IAWjmLJXdmnRcHKeLQzpgtNYZI6lVw1279pqIkRk2MfKb9aDsVFzm565azK5A==", - "deprecated": "We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser", + "license": "BSD-3-Clause", "dependencies": { "@formatjs/ecma402-abstract": "1.5.0", "tslib": "^2.0.1" @@ -3989,9 +3724,8 @@ }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "0.2.36", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz", - "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==", "hasInstallScript": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -3999,9 +3733,8 @@ }, "node_modules/@fortawesome/fontawesome-svg-core": { "version": "1.2.35", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz", - "integrity": "sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg==", "hasInstallScript": true, + "license": "MIT", "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "^0.2.35" @@ -4012,9 +3745,8 @@ }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "5.15.3", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz", - "integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==", "hasInstallScript": true, + "license": "(CC-BY-4.0 AND MIT)", "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "^0.2.35" @@ -4025,8 +3757,7 @@ }, "node_modules/@fortawesome/react-fontawesome": { "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz", - "integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==", + "license": "MIT", "peer": true, "dependencies": { "prop-types": "^15.7.2" @@ -4038,8 +3769,7 @@ }, "node_modules/@fullhuman/postcss-purgecss": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-5.0.0.tgz", - "integrity": "sha512-onDS/b/2pMRzqSoj4qOs2tYFmOpaspjTAgvACIHMPiicu1ptajiBruTrjBzTKdxWdX0ldaBb7wj8nEaTLyFkJw==", + "license": "MIT", "dependencies": { "purgecss": "^5.0.0" }, @@ -4049,8 +3779,7 @@ }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -4062,8 +3791,7 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -4074,13 +3802,11 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "license": "BSD-3-Clause" }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -4094,16 +3820,14 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -4114,8 +3838,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -4125,8 +3848,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -4139,8 +3861,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -4150,16 +3871,14 @@ }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", - "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "@types/node": "*", @@ -4174,8 +3893,7 @@ }, "node_modules/@jest/console/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4188,8 +3906,7 @@ }, "node_modules/@jest/console/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4203,8 +3920,7 @@ }, "node_modules/@jest/console/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4214,21 +3930,18 @@ }, "node_modules/@jest/console/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@jest/console/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -4246,16 +3959,14 @@ }, "node_modules/@jest/console/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4265,8 +3976,7 @@ }, "node_modules/@jest/core": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", - "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "license": "MIT", "dependencies": { "@jest/console": "^26.6.2", "@jest/reporters": "^26.6.2", @@ -4303,8 +4013,7 @@ }, "node_modules/@jest/core/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4317,8 +4026,7 @@ }, "node_modules/@jest/core/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4332,8 +4040,7 @@ }, "node_modules/@jest/core/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4343,21 +4050,18 @@ }, "node_modules/@jest/core/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@jest/core/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/core/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -4375,8 +4079,7 @@ }, "node_modules/@jest/core/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -4389,16 +4092,14 @@ }, "node_modules/@jest/core/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/core/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4408,8 +4109,7 @@ }, "node_modules/@jest/environment": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", - "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "license": "MIT", "dependencies": { "@jest/fake-timers": "^26.6.2", "@jest/types": "^26.6.2", @@ -4422,9 +4122,8 @@ }, "node_modules/@jest/expect-utils": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", "dev": true, + "license": "MIT", "dependencies": { "jest-get-type": "^29.4.3" }, @@ -4434,8 +4133,7 @@ }, "node_modules/@jest/fake-timers": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", - "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "@sinonjs/fake-timers": "^6.0.1", @@ -4450,8 +4148,7 @@ }, "node_modules/@jest/fake-timers/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4464,8 +4161,7 @@ }, "node_modules/@jest/fake-timers/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4479,8 +4175,7 @@ }, "node_modules/@jest/fake-timers/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4490,21 +4185,18 @@ }, "node_modules/@jest/fake-timers/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@jest/fake-timers/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/fake-timers/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -4522,16 +4214,14 @@ }, "node_modules/@jest/fake-timers/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/fake-timers/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4541,8 +4231,7 @@ }, "node_modules/@jest/globals": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", - "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "license": "MIT", "dependencies": { "@jest/environment": "^26.6.2", "@jest/types": "^26.6.2", @@ -4554,8 +4243,7 @@ }, "node_modules/@jest/globals/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4568,8 +4256,7 @@ }, "node_modules/@jest/globals/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4583,8 +4270,7 @@ }, "node_modules/@jest/globals/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4594,21 +4280,18 @@ }, "node_modules/@jest/globals/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@jest/globals/node_modules/diff-sequences": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/@jest/globals/node_modules/expect": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "ansi-styles": "^4.0.0", @@ -4623,16 +4306,14 @@ }, "node_modules/@jest/globals/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/globals/node_modules/jest-diff": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -4645,16 +4326,14 @@ }, "node_modules/@jest/globals/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/@jest/globals/node_modules/jest-matcher-utils": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^26.6.2", @@ -4667,8 +4346,7 @@ }, "node_modules/@jest/globals/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -4686,16 +4364,14 @@ }, "node_modules/@jest/globals/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/globals/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4705,8 +4381,7 @@ }, "node_modules/@jest/reporters": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", - "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^26.6.2", @@ -4742,8 +4417,7 @@ }, "node_modules/@jest/reporters/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4756,8 +4430,7 @@ }, "node_modules/@jest/reporters/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4771,8 +4444,7 @@ }, "node_modules/@jest/reporters/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4782,21 +4454,18 @@ }, "node_modules/@jest/reporters/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@jest/reporters/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.7.5", "@istanbuljs/schema": "^0.1.2", @@ -4809,24 +4478,21 @@ }, "node_modules/@jest/reporters/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/reporters/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@jest/reporters/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4836,9 +4502,8 @@ }, "node_modules/@jest/schemas": { "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.25.16" }, @@ -4848,8 +4513,7 @@ }, "node_modules/@jest/source-map": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", - "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0", "graceful-fs": "^4.2.4", @@ -4861,16 +4525,14 @@ }, "node_modules/@jest/source-map/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@jest/test-result": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", - "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "license": "MIT", "dependencies": { "@jest/console": "^26.6.2", "@jest/types": "^26.6.2", @@ -4883,8 +4545,7 @@ }, "node_modules/@jest/test-sequencer": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", - "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "license": "MIT", "dependencies": { "@jest/test-result": "^26.6.2", "graceful-fs": "^4.2.4", @@ -4898,8 +4559,7 @@ }, "node_modules/@jest/transform": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", - "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "license": "MIT", "dependencies": { "@babel/core": "^7.1.0", "@jest/types": "^26.6.2", @@ -4923,8 +4583,7 @@ }, "node_modules/@jest/transform/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4937,8 +4596,7 @@ }, "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4952,8 +4610,7 @@ }, "node_modules/@jest/transform/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4963,37 +4620,32 @@ }, "node_modules/@jest/transform/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@jest/transform/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/transform/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/transform/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@jest/transform/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5003,8 +4655,7 @@ }, "node_modules/@jest/types": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", @@ -5018,8 +4669,7 @@ }, "node_modules/@jest/types/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -5032,8 +4682,7 @@ }, "node_modules/@jest/types/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5047,8 +4696,7 @@ }, "node_modules/@jest/types/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -5058,21 +4706,18 @@ }, "node_modules/@jest/types/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/@jest/types/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/types/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5082,8 +4727,7 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -5094,24 +4738,21 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -5119,8 +4760,7 @@ }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -5132,13 +4772,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -5146,13 +4784,11 @@ }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + "license": "MIT" }, "node_modules/@newrelic/publish-sourcemap": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@newrelic/publish-sourcemap/-/publish-sourcemap-5.1.0.tgz", - "integrity": "sha512-pOpW0InKZp/DXUmD3h6vaCGdtMDY5LyzzKvq3S3MBwTKm5Qc5ka3yZC73sLAMOXnjKZmdyG3d8A5LC+LawOEpA==", + "license": "New Relic proprietary", "dependencies": { "superagent": "^3.4.1", "yargs": "^16.0.3" @@ -5165,14 +4801,12 @@ }, "node_modules/@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "license": "MIT", "optional": true }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -5183,16 +4817,14 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -5203,8 +4835,7 @@ }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", - "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==", + "license": "MIT", "dependencies": { "ansi-html-community": "^0.0.8", "common-path-prefix": "^3.0.0", @@ -5252,21 +4883,18 @@ }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/@polka/url": { "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" + "license": "MIT" }, "node_modules/@popperjs/core": { "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -5274,16 +4902,14 @@ }, "node_modules/@restart/context": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "license": "MIT", "peerDependencies": { "react": ">=16.3.2" } }, "node_modules/@restart/hooks": { "version": "0.4.7", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.7.tgz", - "integrity": "sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==", + "license": "MIT", "dependencies": { "dequal": "^2.0.2" }, @@ -5293,30 +4919,26 @@ }, "node_modules/@sinclair/typebox": { "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@sinonjs/commons": { "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.5.tgz", - "integrity": "sha512-rTpCA0wG1wUxglBSFdMMY0oTrKYvgf4fNgv/sXbfCVAdf+FnPBdKJR/7XbpTCwbCrvCbdPYnlWaUUYz4V2fPDA==", + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.7.0" } }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz", - "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5330,8 +4952,7 @@ }, "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", "engines": { "node": ">=14" }, @@ -5345,8 +4966,7 @@ }, "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", - "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", "engines": { "node": ">=14" }, @@ -5360,8 +4980,7 @@ }, "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz", - "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5375,8 +4994,7 @@ }, "node_modules/@svgr/babel-plugin-svg-dynamic-title": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz", - "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5390,8 +5008,7 @@ }, "node_modules/@svgr/babel-plugin-svg-em-dimensions": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz", - "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5405,8 +5022,7 @@ }, "node_modules/@svgr/babel-plugin-transform-react-native-svg": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz", - "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5420,8 +5036,7 @@ }, "node_modules/@svgr/babel-plugin-transform-svg-component": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz", - "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -5435,8 +5050,7 @@ }, "node_modules/@svgr/babel-preset": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz", - "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==", + "license": "MIT", "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1", "@svgr/babel-plugin-remove-jsx-attribute": "*", @@ -5460,8 +5074,7 @@ }, "node_modules/@svgr/core": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz", - "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", + "license": "MIT", "dependencies": { "@babel/core": "^7.19.6", "@svgr/babel-preset": "^6.5.1", @@ -5479,8 +5092,7 @@ }, "node_modules/@svgr/hast-util-to-babel-ast": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz", - "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==", + "license": "MIT", "dependencies": { "@babel/types": "^7.20.0", "entities": "^4.4.0" @@ -5495,8 +5107,7 @@ }, "node_modules/@svgr/plugin-jsx": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz", - "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==", + "license": "MIT", "dependencies": { "@babel/core": "^7.19.6", "@svgr/babel-preset": "^6.5.1", @@ -5516,8 +5127,7 @@ }, "node_modules/@svgr/plugin-svgo": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz", - "integrity": "sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==", + "license": "MIT", "dependencies": { "cosmiconfig": "^7.0.1", "deepmerge": "^4.2.2", @@ -5536,8 +5146,7 @@ }, "node_modules/@svgr/webpack": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-6.5.1.tgz", - "integrity": "sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==", + "license": "MIT", "dependencies": { "@babel/core": "^7.19.6", "@babel/plugin-transform-react-constant-elements": "^7.18.12", @@ -5558,8 +5167,7 @@ }, "node_modules/@tanstack/match-sorter-utils": { "version": "8.8.4", - "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", - "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "license": "MIT", "dependencies": { "remove-accents": "0.4.2" }, @@ -5573,8 +5181,7 @@ }, "node_modules/@tanstack/query-core": { "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", - "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -5582,8 +5189,7 @@ }, "node_modules/@tanstack/react-query": { "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", - "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "license": "MIT", "dependencies": { "@tanstack/query-core": "4.36.1", "use-sync-external-store": "^1.2.0" @@ -5608,8 +5214,7 @@ }, "node_modules/@tanstack/react-query-devtools": { "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz", - "integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==", + "license": "MIT", "dependencies": { "@tanstack/match-sorter-utils": "^8.7.0", "superjson": "^1.10.0", @@ -5627,9 +5232,8 @@ }, "node_modules/@testing-library/dom": { "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5646,9 +5250,8 @@ }, "node_modules/@testing-library/dom/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -5661,9 +5264,8 @@ }, "node_modules/@testing-library/dom/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5677,9 +5279,8 @@ }, "node_modules/@testing-library/dom/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -5689,24 +5290,21 @@ }, "node_modules/@testing-library/dom/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@testing-library/dom/node_modules/pretty-format": { "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5718,9 +5316,8 @@ }, "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -5730,15 +5327,13 @@ }, "node_modules/@testing-library/dom/node_modules/react-is": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@testing-library/dom/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5748,9 +5343,8 @@ }, "node_modules/@testing-library/jest-dom": { "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", - "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", "dev": true, + "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", @@ -5770,9 +5364,8 @@ }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -5785,9 +5378,8 @@ }, "node_modules/@testing-library/jest-dom/node_modules/chalk": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5798,9 +5390,8 @@ }, "node_modules/@testing-library/jest-dom/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -5810,24 +5401,21 @@ }, "node_modules/@testing-library/jest-dom/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@testing-library/jest-dom/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@testing-library/jest-dom/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5837,9 +5425,8 @@ }, "node_modules/@testing-library/react": { "version": "11.2.7", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz", - "integrity": "sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^7.28.1" @@ -5854,9 +5441,8 @@ }, "node_modules/@testing-library/react-hooks": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-5.0.3.tgz", - "integrity": "sha512-UrnnRc5II7LMH14xsYNm/WRch/67cBafmrSQcyFh0v+UUmSf1uzfB7zn5jQXSettGwOSxJwdQUN7PgkT0w22Lg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "@types/react": ">=16.9.0", @@ -5881,9 +5467,8 @@ }, "node_modules/@testing-library/react/node_modules/@testing-library/dom": { "version": "7.31.2", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", - "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5900,15 +5485,13 @@ }, "node_modules/@testing-library/react/node_modules/@types/aria-query": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@testing-library/react/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -5921,9 +5504,8 @@ }, "node_modules/@testing-library/react/node_modules/aria-query": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.10.2", "@babel/runtime-corejs3": "^7.10.2" @@ -5934,9 +5516,8 @@ }, "node_modules/@testing-library/react/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5950,9 +5531,8 @@ }, "node_modules/@testing-library/react/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -5962,24 +5542,21 @@ }, "node_modules/@testing-library/react/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@testing-library/react/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@testing-library/react/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5989,9 +5566,8 @@ }, "node_modules/@testing-library/user-event": { "version": "12.8.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz", - "integrity": "sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -6005,30 +5581,25 @@ }, "node_modules/@tootallnate/once": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/@trysound/sax": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", "engines": { "node": ">=10.13.0" } }, "node_modules/@types/aria-query": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.1.20", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz", - "integrity": "sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0", @@ -6039,16 +5610,14 @@ }, "node_modules/@types/babel__generator": { "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -6056,16 +5625,14 @@ }, "node_modules/@types/babel__traverse": { "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "license": "MIT", "dependencies": { "@babel/types": "^7.3.0" } }, "node_modules/@types/body-parser": { "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -6073,24 +5640,21 @@ }, "node_modules/@types/bonjour": { "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -6098,13 +5662,11 @@ }, "node_modules/@types/cookie": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + "license": "MIT" }, "node_modules/@types/eslint": { "version": "8.4.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", - "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6112,8 +5674,7 @@ }, "node_modules/@types/eslint-scope": { "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -6121,13 +5682,11 @@ }, "node_modules/@types/estree": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.16", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.16.tgz", - "integrity": "sha512-LkKpqRZ7zqXJuvoELakaFYuETHjZkSol8EV6cNnyishutDBCCdv6+dsKPbKkCcIk57qRphOLY5sEgClw1bO3gA==", + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.31", @@ -6137,8 +5696,7 @@ }, "node_modules/@types/express-serve-static-core": { "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -6147,16 +5705,14 @@ }, "node_modules/@types/fs-extra": { "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "license": "MIT", "dependencies": { "@types/minimatch": "*", "@types/node": "*" @@ -6164,24 +5720,21 @@ }, "node_modules/@types/graceful-fs": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/hast": { "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", - "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "license": "MIT", "dependencies": { "@types/unist": "*" } }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "license": "MIT", "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -6189,49 +5742,42 @@ }, "node_modules/@types/html-minifier-terser": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", - "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", + "license": "MIT", "peer": true }, "node_modules/@types/http-proxy": { "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/invariant": { "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" + "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { "version": "29.5.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz", - "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -6239,9 +5785,8 @@ }, "node_modules/@types/jest/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6251,9 +5796,8 @@ }, "node_modules/@types/jest/node_modules/pretty-format": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", @@ -6265,77 +5809,63 @@ }, "node_modules/@types/jest/node_modules/react-is": { "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + "license": "MIT" }, "node_modules/@types/mdast": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "license": "MIT", "dependencies": { "@types/unist": "*" } }, "node_modules/@types/mime": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + "license": "MIT" }, "node_modules/@types/minimatch": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + "license": "MIT" }, "node_modules/@types/node": { "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "license": "MIT" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + "license": "MIT" }, "node_modules/@types/parse-json": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "license": "MIT" }, "node_modules/@types/prettier": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==" + "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "license": "MIT" }, "node_modules/@types/qs": { "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "license": "MIT" }, "node_modules/@types/react": { "version": "18.0.25", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", - "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", + "license": "MIT", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6344,17 +5874,14 @@ }, "node_modules/@types/react-dom": { "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", - "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", - "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-redux": { "version": "7.1.24", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", - "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", + "license": "MIT", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -6364,57 +5891,48 @@ }, "node_modules/@types/react-test-renderer": { "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz", - "integrity": "sha512-C7/5FBJ3g3sqUahguGi03O79b8afNeSD6T8/GU50oQrJCU0bVCCGQHaGKUbg2Ce8VQEEqTw8/HiS6lXHHdgkdQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-transition-group": { "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "license": "MIT", "dependencies": { "@types/react": "*" } }, "node_modules/@types/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + "license": "MIT" }, "node_modules/@types/scheduler": { "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + "license": "MIT" }, "node_modules/@types/schema-utils": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/schema-utils/-/schema-utils-2.4.0.tgz", - "integrity": "sha512-454hrj5gz/FXcUE20ygfEiN4DxZ1sprUo0V1gqIqkNZ/CzoEzAZEll2uxMsuyz6BYjiQan4Aa65xbTemfzW9hQ==", - "deprecated": "This is a stub types definition. schema-utils provides its own type definitions, so you do not need this installed.", + "license": "MIT", "dependencies": { "schema-utils": "*" } }, "node_modules/@types/semver": { "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==" + "license": "MIT" }, "node_modules/@types/serve-index": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "license": "MIT", "dependencies": { "@types/mime": "*", "@types/node": "*" @@ -6422,66 +5940,56 @@ }, "node_modules/@types/sockjs": { "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/source-list-map": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" + "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" + "license": "MIT" }, "node_modules/@types/tapable": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==" + "license": "MIT" }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.6", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.6.tgz", - "integrity": "sha512-FkHXCb+ikSoUP4Y4rOslzTdX5sqYwMxfefKh1GmZ8ce1GOkEHntSp6b5cGadmNfp5e4BMEWOMx+WSKd5/MqlDA==", "dev": true, + "license": "MIT", "dependencies": { "@types/jest": "*" } }, "node_modules/@types/uglify-js": { "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", - "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==", + "license": "MIT", "dependencies": { "source-map": "^0.6.1" } }, "node_modules/@types/uglify-js/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@types/unist": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + "license": "MIT" }, "node_modules/@types/warning": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" + "license": "MIT" }, "node_modules/@types/webpack": { "version": "4.41.33", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz", - "integrity": "sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==", + "license": "MIT", "dependencies": { "@types/node": "*", "@types/tapable": "^1", @@ -6493,8 +6001,7 @@ }, "node_modules/@types/webpack-sources": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "license": "MIT", "dependencies": { "@types/node": "*", "@types/source-list-map": "*", @@ -6503,45 +6010,39 @@ }, "node_modules/@types/webpack-sources/node_modules/source-map": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/@types/webpack/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/@types/ws": { "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/yargs": { "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz", - "integrity": "sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==", + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.59.9", @@ -6573,8 +6074,7 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6587,8 +6087,7 @@ }, "node_modules/@typescript-eslint/parser": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.9.tgz", - "integrity": "sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==", + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "5.59.9", "@typescript-eslint/types": "5.59.9", @@ -6613,8 +6112,7 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz", - "integrity": "sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ==", + "license": "MIT", "dependencies": { "@typescript-eslint/types": "5.59.9", "@typescript-eslint/visitor-keys": "5.59.9" @@ -6629,8 +6127,7 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz", - "integrity": "sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==", + "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "5.59.9", "@typescript-eslint/utils": "5.59.9", @@ -6655,8 +6152,7 @@ }, "node_modules/@typescript-eslint/types": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.9.tgz", - "integrity": "sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw==", + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -6667,8 +6163,7 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz", - "integrity": "sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA==", + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "5.59.9", "@typescript-eslint/visitor-keys": "5.59.9", @@ -6693,16 +6188,14 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -6720,8 +6213,7 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6734,16 +6226,14 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@typescript-eslint/utils": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.9.tgz", - "integrity": "sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==", + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", @@ -6767,8 +6257,7 @@ }, "node_modules/@typescript-eslint/utils/node_modules/semver": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6781,8 +6270,7 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "5.59.9", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz", - "integrity": "sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q==", + "license": "MIT", "dependencies": { "@typescript-eslint/types": "5.59.9", "eslint-visitor-keys": "^3.3.0" @@ -6797,8 +6285,7 @@ }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.11.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.1" @@ -6806,23 +6293,19 @@ }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.1", "@webassemblyjs/helper-api-error": "1.11.1", @@ -6831,13 +6314,11 @@ }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -6847,29 +6328,25 @@ }, "node_modules/@webassemblyjs/ieee754": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -6883,8 +6360,7 @@ }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.1", @@ -6895,8 +6371,7 @@ }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -6906,8 +6381,7 @@ }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-api-error": "1.11.1", @@ -6919,8 +6393,7 @@ }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@xtuc/long": "4.2.2" @@ -6928,8 +6401,7 @@ }, "node_modules/@webpack-cli/configtest": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -6940,8 +6412,7 @@ }, "node_modules/@webpack-cli/info": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -6952,8 +6423,7 @@ }, "node_modules/@webpack-cli/serve": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -6967,15 +6437,56 @@ } } }, + "node_modules/@wojtekmaj/enzyme-adapter-react-17": { + "version": "0.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@wojtekmaj/enzyme-adapter-utils": "^0.2.0", + "enzyme-shallow-equal": "^1.0.0", + "has": "^1.0.0", + "prop-types": "^15.7.0", + "react-is": "^17.0.0", + "react-test-renderer": "^17.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/enzyme-adapter-react-17?sponsor=1" + }, + "peerDependencies": { + "enzyme": "^3.0.0", + "react": "^17.0.0-0", + "react-dom": "^17.0.0-0" + } + }, + "node_modules/@wojtekmaj/enzyme-adapter-react-17/node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@wojtekmaj/enzyme-adapter-utils": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "function.prototype.name": "^1.1.0", + "has": "^1.0.0", + "object.fromentries": "^2.0.0", + "prop-types": "^15.7.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/enzyme-adapter-utils?sponsor=1" + }, + "peerDependencies": { + "react": "^17.0.0-0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "license": "Apache-2.0" }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", @@ -6985,13 +6496,11 @@ }, "node_modules/abab": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + "license": "BSD-3-Clause" }, "node_modules/accepts": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -7002,8 +6511,7 @@ }, "node_modules/acorn": { "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -7013,8 +6521,7 @@ }, "node_modules/acorn-globals": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "license": "MIT", "dependencies": { "acorn": "^7.1.1", "acorn-walk": "^7.1.1" @@ -7022,8 +6529,7 @@ }, "node_modules/acorn-globals/node_modules/acorn": { "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -7033,40 +6539,35 @@ }, "node_modules/acorn-import-assertions": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "license": "MIT", "peerDependencies": { "acorn": "^8" } }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/acorn-walk": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/address": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", - "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", + "license": "MIT", "engines": { "node": ">= 0.12.0" } }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "regex-parser": "^2.2.11" @@ -7077,8 +6578,7 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", "dependencies": { "debug": "4" }, @@ -7086,33 +6586,9 @@ "node": ">= 6.0.0" } }, - "node_modules/airbnb-prop-types": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", - "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", - "dev": true, - "dependencies": { - "array.prototype.find": "^2.1.1", - "function.prototype.name": "^1.1.2", - "is-regex": "^1.1.0", - "object-is": "^1.1.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.2", - "prop-types": "^15.7.2", - "prop-types-exact": "^1.2.0", - "react-is": "^16.13.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - }, - "peerDependencies": { - "react": "^0.14 || ^15.0.0 || ^16.0.0-alpha" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/ajv": { + "version": "6.12.6", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7126,8 +6602,7 @@ }, "node_modules/ajv-errors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "license": "MIT", "peer": true, "peerDependencies": { "ajv": ">=5.0.0" @@ -7135,8 +6610,7 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -7151,8 +6625,7 @@ }, "node_modules/ajv-formats/node_modules/ajv": { "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -7166,21 +6639,18 @@ }, "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "license": "MIT" }, "node_modules/ajv-keywords": { "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/algoliasearch": { "version": "4.8.3", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.3.tgz", - "integrity": "sha512-pljX9jEE2TQ3i1JayhG8afNdE8UuJg3O9c7unW6QO67yRWCKr6b0t5aKC3hSVtjt7pA2TQXLKoAISb4SHx9ozQ==", + "license": "MIT", "dependencies": { "@algolia/cache-browser-local-storage": "4.8.3", "@algolia/cache-common": "4.8.3", @@ -7200,8 +6670,7 @@ }, "node_modules/algoliasearch-helper": { "version": "3.11.1", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.11.1.tgz", - "integrity": "sha512-mvsPN3eK4E0bZG0/WlWJjeqe/bUD2KOEVOl0GyL/TGXn6wcpZU8NOuztGHCUKXkyg5gq6YzUakVTmnmSSO5Yiw==", + "license": "MIT", "dependencies": { "@algolia/events": "^4.0.1" }, @@ -7211,8 +6680,7 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -7225,27 +6693,24 @@ }, "node_modules/ansi-html-community": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "engines": [ "node >= 0.8.0" ], + "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" } }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -7255,8 +6720,7 @@ }, "node_modules/anymatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -7267,16 +6731,14 @@ }, "node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/aria-hidden": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.2.tgz", - "integrity": "sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -7295,40 +6757,35 @@ }, "node_modules/aria-query": { "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "license": "Apache-2.0", "dependencies": { "deep-equal": "^2.0.5" } }, "node_modules/arr-diff": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/arr-flatten": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/arr-union": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -7339,13 +6796,11 @@ }, "node_modules/array-flatten": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -7362,8 +6817,7 @@ }, "node_modules/array-union": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "license": "MIT", "dependencies": { "array-uniq": "^1.0.1" }, @@ -7373,25 +6827,22 @@ }, "node_modules/array-uniq": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/array-unique": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/array.prototype.filter": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.2.tgz", - "integrity": "sha512-us+UrmGOilqttSOgoWZTpOvHu68vZT2YCjc/H4vhu56vzZpaDFBhB+Se2UwqWzMKbDv7Myq5M5pcZLAtUvTQdQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -7406,25 +6857,9 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.find": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.1.tgz", - "integrity": "sha512-I2ri5Z9uMpMvnsNrHre9l3PaX+z9D0/z6F7Yt2u15q7wt0I62g5kX6xUKR1SJiefgG+u2/gJUmM8B47XRvQR6w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.flat": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -7440,8 +6875,7 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -7457,8 +6891,7 @@ }, "node_modules/array.prototype.reduce": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "license": "MIT", "peer": true, "dependencies": { "call-bind": "^1.0.2", @@ -7476,8 +6909,7 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -7488,39 +6920,33 @@ }, "node_modules/assert-ok": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-ok/-/assert-ok-1.0.0.tgz", - "integrity": "sha512-lCvYmCpMl8c1tp9ynExhoDEk0gGW43SVVC3RE1VYrrVKhNMy8GHfdiwZdoIM6a605s56bUAbENQxtOC0uZp3wg==" + "license": "MIT" }, "node_modules/assign-symbols": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/ast-types-flow": { "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + "license": "ISC" }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", "engines": { "node": ">= 4.0.0" } }, "node_modules/atob": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", "bin": { "atob": "bin/atob.js" }, @@ -7530,16 +6956,13 @@ }, "node_modules/attr-accept": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", - "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/autoprefixer": { "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", "funding": [ { "type": "opencollective", @@ -7550,6 +6973,7 @@ "url": "https://tidelift.com/funding/github/npm/autoprefixer" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.21.5", "caniuse-lite": "^1.0.30001464", @@ -7570,8 +6994,7 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7581,16 +7004,14 @@ }, "node_modules/axe-core": { "version": "4.7.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", - "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==", + "license": "MPL-2.0", "engines": { "node": ">=4" } }, "node_modules/axios": { "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" @@ -7598,8 +7019,7 @@ }, "node_modules/axios-cache-interceptor": { "version": "0.10.7", - "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-0.10.7.tgz", - "integrity": "sha512-UjpxChG5DpF6Kf1IPGMLOzRDNL8ZNS6TOn1jTaVvCE7cWFU904jJwi0T1s+IbijpnLEjK2iq5uLIuR8Sj+RsFQ==", + "license": "MIT", "dependencies": { "cache-parser": "^1.2.4", "fast-defer": "^1.1.7", @@ -7611,8 +7031,7 @@ }, "node_modules/axios-mock-adapter": { "version": "1.19.0", - "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz", - "integrity": "sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "is-buffer": "^2.0.3" @@ -7623,8 +7042,7 @@ }, "node_modules/axios/node_modules/form-data": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7636,16 +7054,14 @@ }, "node_modules/axobject-query": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", - "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "license": "Apache-2.0", "dependencies": { "deep-equal": "^2.0.5" } }, "node_modules/babel-jest": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", - "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "license": "MIT", "dependencies": { "@jest/transform": "^26.6.2", "@jest/types": "^26.6.2", @@ -7665,8 +7081,7 @@ }, "node_modules/babel-jest/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -7679,8 +7094,7 @@ }, "node_modules/babel-jest/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7694,8 +7108,7 @@ }, "node_modules/babel-jest/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -7705,29 +7118,25 @@ }, "node_modules/babel-jest/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/babel-jest/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/babel-jest/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/babel-jest/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7737,8 +7146,7 @@ }, "node_modules/babel-loader": { "version": "9.1.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", - "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "license": "MIT", "dependencies": { "find-cache-dir": "^3.3.2", "schema-utils": "^4.0.0" @@ -7753,8 +7161,7 @@ }, "node_modules/babel-loader/node_modules/ajv": { "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -7768,8 +7175,7 @@ }, "node_modules/babel-loader/node_modules/ajv-keywords": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -7779,13 +7185,11 @@ }, "node_modules/babel-loader/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "license": "MIT" }, "node_modules/babel-loader/node_modules/schema-utils": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", - "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -7802,8 +7206,7 @@ }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -7817,8 +7220,7 @@ }, "node_modules/babel-plugin-jest-hoist": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", - "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -7831,8 +7233,7 @@ }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.17.7", "@babel/helper-define-polyfill-provider": "^0.3.3", @@ -7844,8 +7245,7 @@ }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.3.3", "core-js-compat": "^3.25.1" @@ -7856,8 +7256,7 @@ }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.3.3" }, @@ -7867,9 +7266,7 @@ }, "node_modules/babel-plugin-react-intl": { "version": "7.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-react-intl/-/babel-plugin-react-intl-7.9.4.tgz", - "integrity": "sha512-cMKrHEXrw43yT4M89Wbgq8A8N8lffSquj1Piwov/HVukR7jwOw8gf9btXNsQhT27ccyqEwy+M286JQYy0jby2g==", - "deprecated": "this package has been renamed to babel-plugin-formatjs", + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.9.0", "@babel/helper-plugin-utils": "^7.8.3", @@ -7885,8 +7282,7 @@ }, "node_modules/babel-plugin-react-intl/node_modules/schema-utils": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", @@ -7902,8 +7298,7 @@ }, "node_modules/babel-plugin-transform-imports": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-imports/-/babel-plugin-transform-imports-2.0.0.tgz", - "integrity": "sha512-65ewumYJ85QiXdcB/jmiU0y0jg6eL6CdnDqQAqQ8JMOKh1E52VPG3NJzbVKWcgovUR5GBH8IWpCXQ7I8Q3wjgw==", + "license": "ISC", "dependencies": { "@babel/types": "^7.4", "is-valid-path": "^0.1.1" @@ -7911,8 +7306,7 @@ }, "node_modules/babel-polyfill": { "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==", + "license": "MIT", "dependencies": { "babel-runtime": "^6.26.0", "core-js": "^2.5.0", @@ -7921,20 +7315,16 @@ }, "node_modules/babel-polyfill/node_modules/core-js": { "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true + "hasInstallScript": true, + "license": "MIT" }, "node_modules/babel-polyfill/node_modules/regenerator-runtime": { "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==" + "license": "MIT" }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -7955,8 +7345,7 @@ }, "node_modules/babel-preset-jest": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", - "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^26.6.2", "babel-preset-current-node-syntax": "^1.0.0" @@ -7970,8 +7359,7 @@ }, "node_modules/babel-runtime": { "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "license": "MIT", "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -7979,20 +7367,16 @@ }, "node_modules/babel-runtime/node_modules/core-js": { "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true + "hasInstallScript": true, + "license": "MIT" }, "node_modules/babel-runtime/node_modules/regenerator-runtime": { "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "license": "MIT" }, "node_modules/bail": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8000,13 +7384,11 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "license": "MIT" }, "node_modules/base": { "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "license": "MIT", "dependencies": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", @@ -8022,8 +7404,7 @@ }, "node_modules/base/node_modules/define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", "dependencies": { "is-descriptor": "^1.0.0" }, @@ -8033,16 +7414,13 @@ }, "node_modules/base/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -8056,104 +7434,30 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/batch": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + "license": "MIT" }, "node_modules/big.js": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/bin-check/node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "extraneous": true, - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/bin-check/node_modules/execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", - "extraneous": true, - "dependencies": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-check/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "extraneous": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/bin-check/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "extraneous": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/bin-check/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "extraneous": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/bin-check/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "extraneous": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/bl": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -8162,8 +7466,7 @@ }, "node_modules/bl/node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8175,8 +7478,7 @@ }, "node_modules/body-parser": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.4", @@ -8198,29 +7500,25 @@ }, "node_modules/body-parser/node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/bonjour-service": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "license": "MIT", "dependencies": { "array-flatten": "^2.1.2", "dns-equal": "^1.0.0", @@ -8230,13 +7528,10 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + "license": "ISC" }, "node_modules/bootstrap": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", "funding": [ { "type": "github", @@ -8247,6 +7542,7 @@ "url": "https://opencollective.com/bootstrap" } ], + "license": "MIT", "peerDependencies": { "jquery": "1.9.1 - 3", "popper.js": "^1.16.1" @@ -8254,8 +7550,7 @@ }, "node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8263,8 +7558,7 @@ }, "node_modules/braces": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "license": "MIT", "dependencies": { "fill-range": "^7.0.1" }, @@ -8274,13 +7568,10 @@ }, "node_modules/browser-process-hrtime": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + "license": "BSD-2-Clause" }, "node_modules/browserslist": { "version": "4.21.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", - "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", "funding": [ { "type": "opencollective", @@ -8295,6 +7586,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001489", "electron-to-chromium": "^1.4.411", @@ -8310,8 +7602,7 @@ }, "node_modules/bs-logger": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "license": "MIT", "dependencies": { "fast-json-stable-stringify": "2.x" }, @@ -8321,16 +7612,13 @@ }, "node_modules/bser": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } }, "node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -8345,6 +7633,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -8352,21 +7641,18 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "license": "MIT" }, "node_modules/bytes": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/cache-base": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "license": "MIT", "dependencies": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", @@ -8384,16 +7670,14 @@ }, "node_modules/cache-base/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/cache-parser": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.4.tgz", - "integrity": "sha512-O0KwuHuJnbHUrghHi2kGp0SxnWSIBXTYt7M8WVhW0kbPRUNUKoE/Of6e1rRD6AAxmfxFunKnt90yEK09D+sc5g==" + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.5", @@ -8410,16 +7694,14 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/camel-case": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -8427,8 +7709,7 @@ }, "node_modules/camelcase": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -8438,8 +7719,7 @@ }, "node_modules/caniuse-api": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", @@ -8448,9 +7728,7 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001566", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", - "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", + "version": "1.0.30001495", "funding": [ { "type": "opencollective", @@ -8464,12 +7742,12 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/capture-exit": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "license": "ISC", "dependencies": { "rsvp": "^4.8.4" }, @@ -8479,16 +7757,14 @@ }, "node_modules/cast-array": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cast-array/-/cast-array-1.0.1.tgz", - "integrity": "sha512-EiqtV+M9L42wd0IRgYjgVGDq7vdNBUUrdecd03QReJp8pIr59o2A1b0XfP+aCUlzLKx2E7zVetaogeJCtiHa+w==", + "license": "MIT", "dependencies": { "isarray": "0.0.1" } }, "node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -8500,16 +7776,14 @@ }, "node_modules/char-regex": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/character-entities": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8517,8 +7791,7 @@ }, "node_modules/character-entities-legacy": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8526,8 +7799,7 @@ }, "node_modules/character-reference-invalid": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8535,9 +7807,8 @@ }, "node_modules/cheerio": { "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", "dev": true, + "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", @@ -8556,9 +7827,8 @@ }, "node_modules/cheerio-select": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", @@ -8573,14 +7843,13 @@ }, "node_modules/chokidar": { "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "funding": [ { "type": "individual", "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -8599,32 +7868,27 @@ }, "node_modules/chownr": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "license": "ISC" }, "node_modules/chrome-trace-event": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "license": "MIT", "engines": { "node": ">=6.0" } }, "node_modules/ci-info": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cjs-module-lexer": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", - "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==" + "license": "MIT" }, "node_modules/class-utils": { "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "license": "MIT", "dependencies": { "arr-union": "^3.1.0", "define-property": "^0.2.5", @@ -8637,8 +7901,7 @@ }, "node_modules/class-utils/node_modules/define-property": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", "dependencies": { "is-descriptor": "^0.1.0" }, @@ -8648,8 +7911,7 @@ }, "node_modules/class-utils/node_modules/is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -8659,8 +7921,7 @@ }, "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -8670,13 +7931,11 @@ }, "node_modules/class-utils/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/class-utils/node_modules/is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -8686,8 +7945,7 @@ }, "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -8697,8 +7955,7 @@ }, "node_modules/class-utils/node_modules/is-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "license": "MIT", "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -8710,29 +7967,25 @@ }, "node_modules/class-utils/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/class-utils/node_modules/kind-of": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/classnames": { "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + "license": "MIT" }, "node_modules/clean-css": { "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "license": "MIT", "peer": true, "dependencies": { "source-map": "~0.6.0" @@ -8743,8 +7996,7 @@ }, "node_modules/clean-css/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -8752,8 +8004,7 @@ }, "node_modules/clean-webpack-plugin": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", + "license": "MIT", "dependencies": { "@types/webpack": "^4.4.31", "del": "^4.1.1" @@ -8767,8 +8018,7 @@ }, "node_modules/cliui": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -8777,8 +8027,7 @@ }, "node_modules/clone-deep": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -8790,8 +8039,7 @@ }, "node_modules/co": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -8799,13 +8047,11 @@ }, "node_modules/collect-v8-coverage": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" + "license": "MIT" }, "node_modules/collection-visit": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "license": "MIT", "dependencies": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -8816,8 +8062,7 @@ }, "node_modules/color": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", - "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "license": "MIT", "dependencies": { "color-convert": "^1.9.1", "color-string": "^1.5.4" @@ -8825,26 +8070,22 @@ }, "node_modules/color-contrast-checker": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-contrast-checker/-/color-contrast-checker-2.1.0.tgz", - "integrity": "sha512-6Y0aIEej3pwZTVlicIqVzhO6T4izDWouaIXnYoDdTuFFAMQ9nnN0dgHNP9J94jRnH6asjPq1/wzUKxwoNbWtRQ==" + "license": "Apache-2.0" }, "node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", "dependencies": { "color-name": "1.1.3" } }, "node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -8852,13 +8093,11 @@ }, "node_modules/colord": { "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -8868,8 +8107,7 @@ }, "node_modules/comma-separated-tokens": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8877,31 +8115,26 @@ }, "node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/common-path-prefix": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" + "license": "ISC" }, "node_modules/commondir": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + "license": "MIT" }, "node_modules/component-emitter": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "license": "MIT" }, "node_modules/compressible": { "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -8911,8 +8144,7 @@ }, "node_modules/compression": { "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "license": "MIT", "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", @@ -8928,44 +8160,37 @@ }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/compression/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "license": "MIT" }, "node_modules/confusing-browser-globals": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" + "license": "MIT" }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/content-disposition": { "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -8975,39 +8200,33 @@ }, "node_modules/content-type": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/convert-source-map": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "license": "MIT" }, "node_modules/cookie": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "license": "MIT" }, "node_modules/cookiejar": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + "license": "MIT" }, "node_modules/copy-anything": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", "dependencies": { "is-what": "^4.1.8" }, @@ -9020,18 +8239,15 @@ }, "node_modules/copy-descriptor": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/core-js": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.7.0.tgz", - "integrity": "sha512-NwS7fI5M5B85EwpWuIwJN4i/fbisQUwLwiSNUWeXlkAZ0sbBjLEvLvFLf1uzAUV66PcEPt4xCGCmOZSxVf3xzA==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -9039,8 +8255,7 @@ }, "node_modules/core-js-compat": { "version": "3.30.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", - "integrity": "sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==", + "license": "MIT", "dependencies": { "browserslist": "^4.21.5" }, @@ -9051,9 +8266,8 @@ }, "node_modules/core-js-pure": { "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.31.0.tgz", - "integrity": "sha512-/AnE9Y4OsJZicCzIe97JP5XoPKQJfTuEG43aEVLFJGOJpyqELod+pE6LEl63DfG1Mp8wX97LDaDpy1GmLEUxlg==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -9061,13 +8275,11 @@ }, "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -9081,8 +8293,7 @@ }, "node_modules/cross-spawn": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -9094,8 +8305,7 @@ }, "node_modules/css-declaration-sorter": { "version": "6.4.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", - "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", + "license": "ISC", "engines": { "node": "^10 || ^12 || >=14" }, @@ -9105,9 +8315,8 @@ }, "node_modules/css-loader": { "version": "5.2.6", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.6.tgz", - "integrity": "sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "loader-utils": "^2.0.0", @@ -9133,9 +8342,8 @@ }, "node_modules/css-loader/node_modules/semver": { "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9148,14 +8356,12 @@ }, "node_modules/css-mediaquery": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", - "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" + "license": "BSD" }, "node_modules/css-select": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -9169,8 +8375,7 @@ }, "node_modules/css-tree": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -9181,16 +8386,14 @@ }, "node_modules/css-tree/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/css-what": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -9200,14 +8403,12 @@ }, "node_modules/css.escape": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -9217,14 +8418,12 @@ }, "node_modules/cssfontparser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz", - "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/csso": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "license": "MIT", "dependencies": { "css-tree": "^1.1.2" }, @@ -9234,13 +8433,11 @@ }, "node_modules/cssom": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + "license": "MIT" }, "node_modules/cssstyle": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "license": "MIT", "dependencies": { "cssom": "~0.3.6" }, @@ -9250,18 +8447,15 @@ }, "node_modules/cssstyle/node_modules/cssom": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + "license": "MIT" }, "node_modules/csstype": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + "license": "BSD-2-Clause" }, "node_modules/dash-embedded-component": { "version": "2.0.2", @@ -9289,61 +8483,28 @@ }, "node_modules/dash-embedded-component/node_modules/check-prop-types": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/check-prop-types/-/check-prop-types-1.1.2.tgz", - "integrity": "sha512-hGDrZ1yhRgKuP1yzZ5sUX/PPmlKBLOF1GyF0Z008Sienko3BFZmlCXnmq+npRTIL/WlFCUzThyd+F5PQnnT1ug==", + "license": "MIT", "peerDependencies": { "prop-types": "<=15.6.0" } }, - "node_modules/dash-embedded-component/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/dash-embedded-component/node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/dash-embedded-component/node_modules/redux": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", - "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.9.2" } }, "node_modules/dash-embedded-component/node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/data-urls": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "license": "MIT", "dependencies": { "abab": "^2.0.3", "whatwg-mimetype": "^2.3.0", @@ -9355,13 +8516,11 @@ }, "node_modules/dayjs": { "version": "1.11.9", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", - "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + "license": "MIT" }, "node_modules/debug": { "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -9376,29 +8535,25 @@ }, "node_modules/decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/decimal.js": { "version": "10.4.2", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", - "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==" + "license": "MIT" }, "node_modules/decode-uri-component": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", "engines": { "node": ">=0.10" } }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -9409,36 +8564,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decompress/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "extraneous": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "extraneous": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/deep-diff": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", - "integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==" + "license": "MIT" }, "node_modules/deep-equal": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", - "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -9465,34 +8597,29 @@ }, "node_modules/deep-equal/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "license": "MIT" }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/default-gateway": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "license": "BSD-2-Clause", "dependencies": { "execa": "^5.0.0" }, @@ -9502,8 +8629,7 @@ }, "node_modules/default-gateway/node_modules/execa": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -9524,8 +8650,7 @@ }, "node_modules/default-gateway/node_modules/get-stream": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -9535,8 +8660,7 @@ }, "node_modules/default-gateway/node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -9546,8 +8670,7 @@ }, "node_modules/default-gateway/node_modules/npm-run-path": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -9570,16 +8693,14 @@ }, "node_modules/define-lazy-prop": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/define-properties": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "license": "MIT", "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -9593,8 +8714,7 @@ }, "node_modules/define-property": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "license": "MIT", "dependencies": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" @@ -9605,16 +8725,14 @@ }, "node_modules/define-property/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/del": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "license": "MIT", "dependencies": { "@types/glob": "^7.1.1", "globby": "^6.1.0", @@ -9630,40 +8748,35 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/dependency-graph": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.9.0.tgz", - "integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==", + "license": "MIT", "engines": { "node": ">= 0.6.0" } }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/destroy": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -9671,34 +8784,29 @@ }, "node_modules/detect-libc": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/detect-newline": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + "license": "MIT" }, "node_modules/detect-node-es": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + "license": "MIT" }, "node_modules/detect-port-alt": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "license": "MIT", "dependencies": { "address": "^1.0.1", "debug": "^2.6.0" @@ -9713,35 +8821,30 @@ }, "node_modules/detect-port-alt/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/detect-port-alt/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/diacritics": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", - "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==" + "license": "MIT" }, "node_modules/diff-sequences": { "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/dir-glob": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -9751,19 +8854,16 @@ }, "node_modules/discontinuous-range": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/dns-equal": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" + "license": "MIT" }, "node_modules/dns-packet": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", - "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -9773,8 +8873,7 @@ }, "node_modules/doctrine": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -9784,21 +8883,18 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.14", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", - "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==" + "license": "MIT" }, "node_modules/dom-converter": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", "dependencies": { "utila": "~0.4" } }, "node_modules/dom-helpers": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -9806,8 +8902,7 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -9819,19 +8914,17 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domexception": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "license": "MIT", "dependencies": { "webidl-conversions": "^5.0.0" }, @@ -9841,16 +8934,14 @@ }, "node_modules/domexception/node_modules/webidl-conversions": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", "engines": { "node": ">=8" } }, "node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -9863,8 +8954,7 @@ }, "node_modules/domutils": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -9876,8 +8966,7 @@ }, "node_modules/dot-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -9885,24 +8974,21 @@ }, "node_modules/dotenv": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "license": "BSD-2-Clause", "engines": { "node": ">=8" } }, "node_modules/dotenv-defaults": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz", - "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==", + "license": "MIT", "dependencies": { "dotenv": "^8.2.0" } }, "node_modules/dotenv-webpack": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-3.0.0.tgz", - "integrity": "sha512-HsBTgVbh9E71IdC6QcQTnAuVt11niA5QMKblcBqhVBxP975XPGMqxf21pqgVBPyRKn2GqPh5kSHMgjxeR/HEAA==", + "license": "MIT", "peer": true, "dependencies": { "dotenv-defaults": "^2.0.1" @@ -9913,39 +8999,32 @@ }, "node_modules/duplexer": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.421", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.421.tgz", - "integrity": "sha512-wZOyn3s/aQOtLGAwXMZfteQPN68kgls2wDAnYOA8kCjBvKVrW5RwmWVspxJYTqrcN7Y263XJVsC66VCIGzDO3g==" + "license": "ISC" }, "node_modules/email-prop-type": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/email-prop-type/-/email-prop-type-3.0.1.tgz", - "integrity": "sha512-tONZGMEOOkadp5OBftuVXU8DsceWmINxYK+pqPFB4LT5ODjrPX/esel3WGqbV7d6in5/MnZE4n4QcqOr4gh7dg==", + "license": "MIT", "dependencies": { "email-validator": "^2.0.4" } }, "node_modules/email-validator": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz", - "integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==", "engines": { "node": ">4.0" } }, "node_modules/emittery": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", - "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -9955,37 +9034,32 @@ }, "node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "license": "MIT" }, "node_modules/emojis-list": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/encodeurl": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/end-of-stream": { "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { "version": "5.14.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz", - "integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -9996,16 +9070,14 @@ }, "node_modules/enhanced-resolve/node_modules/tapable": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/entities": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -10015,8 +9087,7 @@ }, "node_modules/envinfo": { "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, @@ -10026,9 +9097,8 @@ }, "node_modules/enzyme": { "version": "3.11.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", - "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", "dev": true, + "license": "MIT", "dependencies": { "array.prototype.flat": "^1.2.3", "cheerio": "^1.0.0-rc.3", @@ -10057,86 +9127,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/enzyme-adapter-react-16": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz", - "integrity": "sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g==", - "dev": true, - "dependencies": { - "enzyme-adapter-utils": "^1.14.0", - "enzyme-shallow-equal": "^1.0.4", - "has": "^1.0.3", - "object.assign": "^4.1.2", - "object.values": "^1.1.2", - "prop-types": "^15.7.2", - "react-is": "^16.13.1", - "react-test-renderer": "^16.0.0-0", - "semver": "^5.7.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - }, - "peerDependencies": { - "enzyme": "^3.0.0", - "react": "^16.0.0-0", - "react-dom": "^16.0.0-0" - } - }, - "node_modules/enzyme-adapter-react-16/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/enzyme-adapter-utils": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz", - "integrity": "sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ==", - "dev": true, - "dependencies": { - "airbnb-prop-types": "^2.16.0", - "function.prototype.name": "^1.1.5", - "has": "^1.0.3", - "object.assign": "^4.1.4", - "object.fromentries": "^2.0.5", - "prop-types": "^15.8.1", - "semver": "^5.7.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - }, - "peerDependencies": { - "react": "0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0" - } - }, - "node_modules/enzyme-adapter-utils/node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/enzyme-adapter-utils/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/enzyme-shallow-equal": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz", - "integrity": "sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg==", "dev": true, + "license": "MIT", "dependencies": { "has": "^1.0.3", "object-is": "^1.1.5" @@ -10147,24 +9141,21 @@ }, "node_modules/error-ex": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/error-stack-parser": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", "dependencies": { "stackframe": "^1.3.4" } }, "node_modules/es-abstract": { "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", "available-typed-arrays": "^1.0.5", @@ -10210,13 +9201,11 @@ }, "node_modules/es-array-method-boxes-properly": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + "license": "MIT" }, "node_modules/es-get-iterator": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -10234,18 +9223,15 @@ }, "node_modules/es-get-iterator/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "license": "MIT" }, "node_modules/es-module-lexer": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==" + "license": "MIT" }, "node_modules/es-set-tostringtag": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3", "has": "^1.0.3", @@ -10257,16 +9243,14 @@ }, "node_modules/es-shim-unscopables": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "license": "MIT", "dependencies": { "has": "^1.0.3" } }, "node_modules/es-to-primitive": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "license": "MIT", "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -10281,34 +9265,29 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + "license": "MIT" }, "node_modules/escalade": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/escodegen": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -10328,8 +9307,7 @@ }, "node_modules/escodegen/node_modules/levn": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -10340,8 +9318,7 @@ }, "node_modules/escodegen/node_modules/optionator": { "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", @@ -10356,16 +9333,13 @@ }, "node_modules/escodegen/node_modules/prelude-ls": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "engines": { "node": ">= 0.8.0" } }, "node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -10373,8 +9347,7 @@ }, "node_modules/escodegen/node_modules/type-check": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", "dependencies": { "prelude-ls": "~1.1.2" }, @@ -10384,8 +9357,7 @@ }, "node_modules/eslint": { "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", @@ -10440,8 +9412,7 @@ }, "node_modules/eslint-config-airbnb": { "version": "19.0.4", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", - "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "license": "MIT", "dependencies": { "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", @@ -10460,8 +9431,7 @@ }, "node_modules/eslint-config-airbnb-base": { "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "license": "MIT", "dependencies": { "confusing-browser-globals": "^1.0.10", "object.assign": "^4.1.2", @@ -10478,8 +9448,7 @@ }, "node_modules/eslint-config-airbnb-typescript": { "version": "17.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.0.0.tgz", - "integrity": "sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==", + "license": "MIT", "dependencies": { "eslint-config-airbnb-base": "^15.0.0" }, @@ -10492,8 +9461,7 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", - "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.11.0", @@ -10502,16 +9470,14 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-module-utils": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -10526,16 +9492,14 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -10562,16 +9526,14 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -10581,8 +9543,7 @@ }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", - "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.7", "aria-query": "^5.1.3", @@ -10610,8 +9571,7 @@ }, "node_modules/eslint-plugin-react": { "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", @@ -10638,8 +9598,7 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -10649,8 +9608,7 @@ }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -10660,8 +9618,7 @@ }, "node_modules/eslint-plugin-react/node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -10670,8 +9627,7 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "license": "MIT", "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -10686,8 +9642,7 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10698,16 +9653,14 @@ }, "node_modules/eslint-scope/node_modules/estraverse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/eslint-visitor-keys": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -10717,8 +9670,7 @@ }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -10731,13 +9683,11 @@ }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "license": "Python-2.0" }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10751,8 +9701,7 @@ }, "node_modules/eslint/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -10762,13 +9711,11 @@ }, "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -10778,8 +9725,7 @@ }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -10790,8 +9736,7 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -10801,8 +9746,7 @@ }, "node_modules/eslint/node_modules/globals": { "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -10815,16 +9759,14 @@ }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/eslint/node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -10834,8 +9776,7 @@ }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -10845,8 +9786,7 @@ }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -10856,8 +9796,7 @@ }, "node_modules/espree": { "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", @@ -10872,8 +9811,7 @@ }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -10884,8 +9822,7 @@ }, "node_modules/esquery": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -10895,8 +9832,7 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -10906,50 +9842,43 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/eventemitter3": { "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", "engines": { "node": ">=0.8.x" } }, "node_modules/exec-sh": { "version": "0.3.6", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", - "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==" + "license": "MIT" }, "node_modules/execa": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "license": "MIT", "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -10965,8 +9894,7 @@ }, "node_modules/execa/node_modules/cross-spawn": { "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -10980,24 +9908,21 @@ }, "node_modules/execa/node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/execa/node_modules/semver": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/execa/node_modules/shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" }, @@ -11007,16 +9932,14 @@ }, "node_modules/execa/node_modules/shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/execa/node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -11026,16 +9949,13 @@ }, "node_modules/exit": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "engines": { "node": ">= 0.8.0" } }, "node_modules/expand-brackets": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "license": "MIT", "dependencies": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -11051,16 +9971,14 @@ }, "node_modules/expand-brackets/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/expand-brackets/node_modules/define-property": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", "dependencies": { "is-descriptor": "^0.1.0" }, @@ -11070,8 +9988,7 @@ }, "node_modules/expand-brackets/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -11081,8 +9998,7 @@ }, "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -11092,8 +10008,7 @@ }, "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -11103,13 +10018,11 @@ }, "node_modules/expand-brackets/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/expand-brackets/node_modules/is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -11119,8 +10032,7 @@ }, "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -11130,8 +10042,7 @@ }, "node_modules/expand-brackets/node_modules/is-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "license": "MIT", "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -11143,38 +10054,33 @@ }, "node_modules/expand-brackets/node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/expand-brackets/node_modules/kind-of": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/expand-brackets/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/expand-template": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" } }, "node_modules/expect": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.5.0", "jest-get-type": "^29.4.3", @@ -11188,9 +10094,8 @@ }, "node_modules/expect/node_modules/@jest/types": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.4.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -11205,18 +10110,16 @@ }, "node_modules/expect/node_modules/@types/yargs": { "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/expect/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -11229,9 +10132,8 @@ }, "node_modules/expect/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -11245,8 +10147,6 @@ }, "node_modules/expect/node_modules/ci-info": { "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true, "funding": [ { @@ -11254,15 +10154,15 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/expect/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -11272,24 +10172,21 @@ }, "node_modules/expect/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/expect/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/expect/node_modules/jest-util": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.5.0", "@types/node": "*", @@ -11304,9 +10201,8 @@ }, "node_modules/expect/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -11316,8 +10212,7 @@ }, "node_modules/express": { "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -11357,44 +10252,37 @@ }, "node_modules/express/node_modules/array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "license": "MIT" }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/express/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/express/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "license": "MIT" }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "license": "MIT" }, "node_modules/extend-shallow": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -11405,8 +10293,7 @@ }, "node_modules/extglob": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "license": "MIT", "dependencies": { "array-unique": "^0.3.2", "define-property": "^1.0.0", @@ -11423,8 +10310,7 @@ }, "node_modules/extglob/node_modules/define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", "dependencies": { "is-descriptor": "^1.0.0" }, @@ -11434,8 +10320,7 @@ }, "node_modules/extglob/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -11445,26 +10330,22 @@ }, "node_modules/extglob/node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "license": "MIT" }, "node_modules/fast-defer": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.7.tgz", - "integrity": "sha512-tJ01ulDWT2WhqxMKS20nXX6wyX2iInBYpbN3GO7yjKwXMY4qvkdBRxak9IFwBLlFDESox+SwSvqMCZDfe1tqeg==" + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -11478,34 +10359,29 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "license": "MIT" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", "engines": { "node": ">= 4.9.1" } }, "node_modules/fastq": { "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/faye-websocket": { "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -11515,16 +10391,14 @@ }, "node_modules/fb-watchman": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" } }, "node_modules/file-entry-cache": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -11534,8 +10408,7 @@ }, "node_modules/file-loader": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" @@ -11553,13 +10426,11 @@ }, "node_modules/file-saver": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz", - "integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==" + "license": "MIT" }, "node_modules/file-selector": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "license": "MIT", "dependencies": { "tslib": "^2.4.0" }, @@ -11569,17 +10440,15 @@ }, "node_modules/filesize": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", - "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">= 0.4.0" } }, "node_modules/fill-range": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -11589,25 +10458,22 @@ }, "node_modules/filter-console": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/filter-console/-/filter-console-0.1.1.tgz", - "integrity": "sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/filter-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/finalhandler": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -11623,21 +10489,18 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/find-cache-dir": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "license": "MIT", "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -11652,8 +10515,7 @@ }, "node_modules/find-cache-dir/node_modules/make-dir": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", "dependencies": { "semver": "^6.0.0" }, @@ -11666,8 +10528,7 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -11690,8 +10551,7 @@ }, "node_modules/flat-cache": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "license": "MIT", "dependencies": { "flatted": "^3.1.0", "rimraf": "^3.0.2" @@ -11702,8 +10562,7 @@ }, "node_modules/flat-cache/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -11716,13 +10575,11 @@ }, "node_modules/flatted": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + "license": "ISC" }, "node_modules/focus-lock": { "version": "0.11.3", - "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.3.tgz", - "integrity": "sha512-4n0pYcPTa/uI7Q66BZna61nRT7lDhnuJ9PJr6wiDjx4uStg491ks41y7uOG+s0umaaa+hulNKSldU9aTg9/yVg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.3" }, @@ -11732,14 +10589,13 @@ }, "node_modules/follow-redirects": { "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -11751,33 +10607,29 @@ }, "node_modules/font-awesome": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", - "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", + "license": "(OFL-1.1 AND MIT)", "engines": { "node": ">=0.10.3" } }, "node_modules/for-each": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "license": "MIT", "dependencies": { "is-callable": "^1.1.3" } }, "node_modules/for-in": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", - "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.5.5", "chalk": "^2.4.1", @@ -11794,9 +10646,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/braces": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, + "license": "MIT", "dependencies": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -11815,9 +10666,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/braces/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -11827,9 +10677,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/fill-range": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "dev": true, + "license": "MIT", "dependencies": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -11842,9 +10691,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/fill-range/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -11854,24 +10702,21 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-number": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -11881,9 +10726,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/is-number/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -11893,18 +10737,16 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/micromatch": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, + "license": "MIT", "dependencies": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -11926,18 +10768,16 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/to-regex-range": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -11948,8 +10788,7 @@ }, "node_modules/form-data": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -11961,30 +10800,25 @@ }, "node_modules/form-urlencoded": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-4.1.4.tgz", - "integrity": "sha512-R7Vytos0gMYuPQTMwnNzvK9PBItNV+Qkm/pvghEZI3j2kMrzZmJlczAgHFmt12VV+IRYQXgTlSGP1PKAsMCIUA==" + "license": "MIT" }, "node_modules/formidable": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "license": "MIT", "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fraction.js": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "license": "MIT", "engines": { "node": "*" }, @@ -11995,8 +10829,7 @@ }, "node_modules/fragment-cache": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "license": "MIT", "dependencies": { "map-cache": "^0.2.2" }, @@ -12006,8 +10839,7 @@ }, "node_modules/fresh": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -12018,13 +10850,11 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "license": "MIT" }, "node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -12037,24 +10867,19 @@ }, "node_modules/fs-monkey": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" + "license": "Unlicense" }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" + "license": "MIT" }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -12073,8 +10898,7 @@ }, "node_modules/function.prototype.name": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", @@ -12090,32 +10914,28 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -12128,24 +10948,21 @@ }, "node_modules/get-nonce": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/get-package-type": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", "engines": { "node": ">=8.0.0" } }, "node_modules/get-stream": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -12155,8 +10972,7 @@ }, "node_modules/get-symbol-description": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -12170,21 +10986,18 @@ }, "node_modules/get-value": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/github-from-package": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "license": "MIT" }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -12202,8 +11015,7 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -12213,13 +11025,11 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "license": "BSD-2-Clause" }, "node_modules/global-modules": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "license": "MIT", "dependencies": { "global-prefix": "^3.0.0" }, @@ -12229,8 +11039,7 @@ }, "node_modules/global-prefix": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "license": "MIT", "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", @@ -12242,8 +11051,7 @@ }, "node_modules/global-prefix/node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -12253,16 +11061,14 @@ }, "node_modules/globals": { "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/globalthis": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "license": "MIT", "dependencies": { "define-properties": "^1.1.3" }, @@ -12275,8 +11081,7 @@ }, "node_modules/globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "license": "MIT", "dependencies": { "array-union": "^1.0.1", "glob": "^7.0.3", @@ -12290,16 +11095,14 @@ }, "node_modules/globby/node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/gopd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -12309,25 +11112,21 @@ }, "node_modules/graceful-fs": { "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "license": "ISC" }, "node_modules/grapheme-splitter": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + "license": "MIT" }, "node_modules/growly": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "license": "MIT", "optional": true }, "node_modules/gzip-size": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", - "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", "dev": true, + "license": "MIT", "dependencies": { "duplexer": "^0.1.1", "pify": "^4.0.1" @@ -12338,18 +11137,15 @@ }, "node_modules/handle-thing": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + "license": "MIT" }, "node_modules/harmony-reflect": { "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" + "license": "(Apache-2.0 OR MPL-1.1)" }, "node_modules/has": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.1" }, @@ -12359,24 +11155,21 @@ }, "node_modules/has-bigints": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/has-property-descriptors": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.1" }, @@ -12386,8 +11179,7 @@ }, "node_modules/has-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -12397,8 +11189,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -12408,8 +11199,7 @@ }, "node_modules/has-tostringtag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" }, @@ -12422,8 +11212,7 @@ }, "node_modules/has-value": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "license": "MIT", "dependencies": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -12435,16 +11224,14 @@ }, "node_modules/has-value/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/has-values": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "license": "MIT", "dependencies": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -12455,13 +11242,11 @@ }, "node_modules/has-values/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/has-values/node_modules/is-number": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -12471,8 +11256,7 @@ }, "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -12482,8 +11266,7 @@ }, "node_modules/has-values/node_modules/kind-of": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -12493,16 +11276,14 @@ }, "node_modules/he": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", "bin": { "he": "bin/he" } }, "node_modules/history": { "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -12514,21 +11295,18 @@ }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } }, "node_modules/hosted-git-info": { "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "license": "ISC" }, "node_modules/hpack.js": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -12538,8 +11316,7 @@ }, "node_modules/html-dom-parser": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-3.1.2.tgz", - "integrity": "sha512-mLTtl3pVn3HnqZSZzW3xVs/mJAKrG1yIw3wlp+9bdoZHHLaBRvELdpfShiPVLyjPypq1Fugv2KMDoGHW4lVXnw==", + "license": "MIT", "dependencies": { "domhandler": "5.0.3", "htmlparser2": "8.0.1" @@ -12547,9 +11324,8 @@ }, "node_modules/html-element-map": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", - "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", "dev": true, + "license": "MIT", "dependencies": { "array.prototype.filter": "^1.0.0", "call-bind": "^1.0.2" @@ -12560,8 +11336,7 @@ }, "node_modules/html-encoding-sniffer": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "license": "MIT", "dependencies": { "whatwg-encoding": "^1.0.5" }, @@ -12571,18 +11346,15 @@ }, "node_modules/html-entities": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" + "license": "MIT" }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "license": "MIT" }, "node_modules/html-minifier-terser": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "license": "MIT", "peer": true, "dependencies": { "camel-case": "^4.1.1", @@ -12602,8 +11374,7 @@ }, "node_modules/html-react-parser": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-3.0.7.tgz", - "integrity": "sha512-4Nzpp1Lsd6ngOJR8T+Vc4u+Z77OddOgKL3KvwbtA0/U0Yv8v5JF+yewQxIudrdOWGYuO0Borc0vQ2y53pzBAwA==", + "license": "MIT", "dependencies": { "domhandler": "5.0.3", "html-dom-parser": "3.1.2", @@ -12616,8 +11387,7 @@ }, "node_modules/html-webpack-plugin": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz", - "integrity": "sha512-MouoXEYSjTzCrjIxWwg8gxL5fE2X2WZJLmBYXlaJhQUH5K/b5OrqmV7T4dB7iu0xkmJ6JlUuV6fFVtnqbPopZw==", + "license": "MIT", "peer": true, "dependencies": { "@types/html-minifier-terser": "^5.0.0", @@ -12639,8 +11409,7 @@ }, "node_modules/html-webpack-plugin/node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", "peer": true, "dependencies": { "minimist": "^1.2.0" @@ -12651,8 +11420,7 @@ }, "node_modules/html-webpack-plugin/node_modules/loader-utils": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "license": "MIT", "peer": true, "dependencies": { "big.js": "^5.2.2", @@ -12665,8 +11433,6 @@ }, "node_modules/htmlparser2": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -12674,6 +11440,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -12683,13 +11450,11 @@ }, "node_modules/http-deceiver": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" + "license": "MIT" }, "node_modules/http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -12703,13 +11468,11 @@ }, "node_modules/http-parser-js": { "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -12721,8 +11484,7 @@ }, "node_modules/http-proxy-agent": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", "dependencies": { "@tootallnate/once": "1", "agent-base": "6", @@ -12734,8 +11496,7 @@ }, "node_modules/http-proxy-middleware": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -12757,8 +11518,7 @@ }, "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -12768,8 +11528,7 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", "dependencies": { "agent-base": "6", "debug": "4" @@ -12780,18 +11539,16 @@ }, "node_modules/human-signals": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/husky": { "version": "0.14.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", - "integrity": "sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "is-ci": "^1.0.10", "normalize-path": "^1.0.0", @@ -12803,22 +11560,19 @@ }, "node_modules/husky/node_modules/normalize-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", - "integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/hyphenate-style-name": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", - "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + "license": "BSD-3-Clause" }, "node_modules/i18n-iso-countries": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-4.3.1.tgz", - "integrity": "sha512-yxeCvmT8yO1p/epv93c1OHnnYNNMOX6NUNpNfuvzSIcDyripS7OGeKXgzYGd5QI31UK+GBrMG0nPFNv0jrHggw==", + "license": "MIT", "dependencies": { "diacritics": "^1.3.0" }, @@ -12828,8 +11582,7 @@ }, "node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -12839,8 +11592,7 @@ }, "node_modules/icss-utils": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -12850,8 +11602,7 @@ }, "node_modules/identity-obj-proxy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "license": "MIT", "dependencies": { "harmony-reflect": "^1.4.6" }, @@ -12861,8 +11612,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -12876,20 +11625,19 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/image-minimizer-webpack-plugin": { "version": "3.8.2", - "resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-3.8.2.tgz", - "integrity": "sha512-l3nDq/c15y4ViTPtICG6lbFp77SoycSnR1hT/n3ER76uol//OpRptCDl7U1qiDSSEy2AcqPD1T7isRQ8TK27Cw==", + "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", "serialize-javascript": "^6.0.1" @@ -12921,8 +11669,7 @@ }, "node_modules/image-minimizer-webpack-plugin/node_modules/ajv": { "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -12936,8 +11683,7 @@ }, "node_modules/image-minimizer-webpack-plugin/node_modules/ajv-keywords": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12947,13 +11693,11 @@ }, "node_modules/image-minimizer-webpack-plugin/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "license": "MIT" }, "node_modules/image-minimizer-webpack-plugin/node_modules/schema-utils": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", - "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -12968,101 +11712,14 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/imagemin-mozjpeg/node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "extraneous": true, - "engines": { - "node": ">=8.12.0" - } - }, - "node_modules/imagemin-mozjpeg/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "extraneous": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imagemin-mozjpeg/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "extraneous": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/imagemin/node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "extraneous": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/imagemin/node_modules/globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", - "extraneous": true, - "dependencies": { - "@types/glob": "^7.1.1", - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/imagemin/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "extraneous": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imagemin/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "extraneous": true, - "engines": { - "node": ">=8" - } - }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "license": "MIT" }, "node_modules/immer": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", "dev": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -13070,13 +11727,11 @@ }, "node_modules/immutable": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", - "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==" + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -13090,16 +11745,14 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/import-local": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -13116,25 +11769,22 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", "engines": { "node": ">=0.8.19" } }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -13142,23 +11792,19 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "license": "ISC" }, "node_modules/inline-style-parser": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + "license": "MIT" }, "node_modules/internal-slot": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -13170,16 +11816,14 @@ }, "node_modules/interpret": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/intl-messageformat": { "version": "9.13.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", - "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", + "license": "BSD-3-Clause", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/fast-memoize": "1.2.1", @@ -13189,33 +11833,28 @@ }, "node_modules/intl-messageformat-parser": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-5.5.1.tgz", - "integrity": "sha512-TvB3LqF2VtP6yI6HXlRT5TxX98HKha6hCcrg9dwlPwNaedVNuQA9KgBdtWKgiyakyCTYHQ+KJeFEstNKfZr64w==", - "deprecated": "We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser", + "license": "BSD-3-Clause", "dependencies": { "@formatjs/intl-numberformat": "^5.5.2" } }, "node_modules/invariant": { "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/ipaddr.js": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/is-accessor-descriptor": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "license": "MIT", "dependencies": { "kind-of": "^6.0.0" }, @@ -13225,8 +11864,7 @@ }, "node_modules/is-alphabetical": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13234,8 +11872,7 @@ }, "node_modules/is-alphanumerical": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -13247,8 +11884,7 @@ }, "node_modules/is-arguments": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -13262,8 +11898,7 @@ }, "node_modules/is-array-buffer": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -13275,13 +11910,11 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "license": "MIT" }, "node_modules/is-bigint": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" }, @@ -13291,8 +11924,7 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -13302,8 +11934,7 @@ }, "node_modules/is-boolean-object": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -13317,8 +11948,6 @@ }, "node_modules/is-buffer": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "funding": [ { "type": "github", @@ -13333,14 +11962,14 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -13350,9 +11979,8 @@ }, "node_modules/is-ci": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "dev": true, + "license": "MIT", "dependencies": { "ci-info": "^1.5.0" }, @@ -13362,8 +11990,7 @@ }, "node_modules/is-core-module": { "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "license": "MIT", "dependencies": { "has": "^1.0.3" }, @@ -13373,8 +12000,7 @@ }, "node_modules/is-data-descriptor": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "license": "MIT", "dependencies": { "kind-of": "^6.0.0" }, @@ -13384,8 +12010,7 @@ }, "node_modules/is-date-object": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -13398,8 +12023,7 @@ }, "node_modules/is-decimal": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13407,8 +12031,7 @@ }, "node_modules/is-descriptor": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "license": "MIT", "dependencies": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -13420,8 +12043,7 @@ }, "node_modules/is-docker": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -13434,8 +12056,7 @@ }, "node_modules/is-extendable": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4" }, @@ -13445,32 +12066,28 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-generator-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -13480,8 +12097,7 @@ }, "node_modules/is-hexadecimal": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13489,8 +12105,7 @@ }, "node_modules/is-invalid-path": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", - "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "license": "MIT", "dependencies": { "is-glob": "^2.0.0" }, @@ -13500,16 +12115,14 @@ }, "node_modules/is-invalid-path/node_modules/is-extglob": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-invalid-path/node_modules/is-glob": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", "dependencies": { "is-extglob": "^1.0.0" }, @@ -13519,16 +12132,14 @@ }, "node_modules/is-map": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-negative-zero": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -13538,16 +12149,14 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-number-object": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -13560,16 +12169,14 @@ }, "node_modules/is-path-cwd": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-path-in-cwd": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "license": "MIT", "dependencies": { "is-path-inside": "^2.1.0" }, @@ -13579,8 +12186,7 @@ }, "node_modules/is-path-in-cwd/node_modules/is-path-inside": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "license": "MIT", "dependencies": { "path-is-inside": "^1.0.2" }, @@ -13590,16 +12196,14 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -13607,8 +12211,7 @@ }, "node_modules/is-plain-object": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -13618,26 +12221,22 @@ }, "node_modules/is-plain-object/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + "license": "MIT" }, "node_modules/is-promise": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + "license": "MIT" }, "node_modules/is-regex": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -13651,24 +12250,21 @@ }, "node_modules/is-root": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-set": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2" }, @@ -13678,16 +12274,14 @@ }, "node_modules/is-stream": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-string": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -13700,14 +12294,12 @@ }, "node_modules/is-subset": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-symbol": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" }, @@ -13720,8 +12312,7 @@ }, "node_modules/is-typed-array": { "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -13738,13 +12329,11 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "license": "MIT" }, "node_modules/is-valid-path": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", - "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "license": "MIT", "dependencies": { "is-invalid-path": "^0.1.0" }, @@ -13754,16 +12343,14 @@ }, "node_modules/is-weakmap": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakref": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2" }, @@ -13773,8 +12360,7 @@ }, "node_modules/is-weakset": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -13785,8 +12371,7 @@ }, "node_modules/is-what": { "version": "4.1.15", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", - "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "license": "MIT", "engines": { "node": ">=12.13" }, @@ -13796,16 +12381,14 @@ }, "node_modules/is-windows": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-wsl": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -13815,26 +12398,22 @@ }, "node_modules/isarray": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -13848,8 +12427,7 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^3.0.0", @@ -13861,16 +12439,14 @@ }, "node_modules/istanbul-lib-report/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-report/node_modules/make-dir": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", "dependencies": { "semver": "^6.0.0" }, @@ -13883,8 +12459,7 @@ }, "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -13894,8 +12469,7 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -13907,16 +12481,14 @@ }, "node_modules/istanbul-lib-source-maps/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/istanbul-reports": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -13927,8 +12499,7 @@ }, "node_modules/jest": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", - "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "license": "MIT", "dependencies": { "@jest/core": "^26.6.3", "import-local": "^3.0.2", @@ -13943,9 +12514,8 @@ }, "node_modules/jest-canvas-mock": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz", - "integrity": "sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==", "dev": true, + "license": "MIT", "dependencies": { "cssfontparser": "^1.2.1", "moo-color": "^1.0.2" @@ -13953,8 +12523,7 @@ }, "node_modules/jest-changed-files": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", - "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "execa": "^4.0.0", @@ -13966,8 +12535,7 @@ }, "node_modules/jest-changed-files/node_modules/execa": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", @@ -13988,8 +12556,7 @@ }, "node_modules/jest-changed-files/node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -14002,16 +12569,14 @@ }, "node_modules/jest-changed-files/node_modules/human-signals": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "license": "Apache-2.0", "engines": { "node": ">=8.12.0" } }, "node_modules/jest-changed-files/node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -14021,8 +12586,7 @@ }, "node_modules/jest-changed-files/node_modules/npm-run-path": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -14032,8 +12596,7 @@ }, "node_modules/jest-cli": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", - "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "license": "MIT", "dependencies": { "@jest/core": "^26.6.3", "@jest/test-result": "^26.6.2", @@ -14058,8 +12621,7 @@ }, "node_modules/jest-cli/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -14072,16 +12634,14 @@ }, "node_modules/jest-cli/node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/jest-cli/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14095,13 +12655,11 @@ }, "node_modules/jest-cli/node_modules/ci-info": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + "license": "MIT" }, "node_modules/jest-cli/node_modules/cliui": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -14110,8 +12668,7 @@ }, "node_modules/jest-cli/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -14121,13 +12678,11 @@ }, "node_modules/jest-cli/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-cli/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -14138,16 +12693,14 @@ }, "node_modules/jest-cli/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-cli/node_modules/is-ci": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "license": "MIT", "dependencies": { "ci-info": "^2.0.0" }, @@ -14157,8 +12710,7 @@ }, "node_modules/jest-cli/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -14168,8 +12720,7 @@ }, "node_modules/jest-cli/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -14182,8 +12733,7 @@ }, "node_modules/jest-cli/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -14193,8 +12743,7 @@ }, "node_modules/jest-cli/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14204,8 +12753,7 @@ }, "node_modules/jest-cli/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -14217,13 +12765,11 @@ }, "node_modules/jest-cli/node_modules/y18n": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + "license": "ISC" }, "node_modules/jest-cli/node_modules/yargs": { "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -14243,8 +12789,7 @@ }, "node_modules/jest-cli/node_modules/yargs-parser": { "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -14255,8 +12800,7 @@ }, "node_modules/jest-config": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", - "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", + "license": "MIT", "dependencies": { "@babel/core": "^7.1.0", "@jest/test-sequencer": "^26.6.3", @@ -14291,8 +12835,7 @@ }, "node_modules/jest-config/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -14305,8 +12848,7 @@ }, "node_modules/jest-config/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14320,8 +12862,7 @@ }, "node_modules/jest-config/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -14331,21 +12872,18 @@ }, "node_modules/jest-config/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-config/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-config/node_modules/jest-environment-jsdom": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", - "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "license": "MIT", "dependencies": { "@jest/environment": "^26.6.2", "@jest/fake-timers": "^26.6.2", @@ -14361,16 +12899,14 @@ }, "node_modules/jest-config/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-config/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14380,9 +12916,8 @@ }, "node_modules/jest-diff": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.4.3", @@ -14395,9 +12930,8 @@ }, "node_modules/jest-diff/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -14410,9 +12944,8 @@ }, "node_modules/jest-diff/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14426,9 +12959,8 @@ }, "node_modules/jest-diff/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -14438,24 +12970,21 @@ }, "node_modules/jest-diff/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-diff/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-diff/node_modules/pretty-format": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", @@ -14467,9 +12996,8 @@ }, "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -14479,15 +13007,13 @@ }, "node_modules/jest-diff/node_modules/react-is": { "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14497,8 +13023,7 @@ }, "node_modules/jest-docblock": { "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", - "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" }, @@ -14508,8 +13033,7 @@ }, "node_modules/jest-each": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", - "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "chalk": "^4.0.0", @@ -14523,8 +13047,7 @@ }, "node_modules/jest-each/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -14537,8 +13060,7 @@ }, "node_modules/jest-each/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14552,8 +13074,7 @@ }, "node_modules/jest-each/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -14563,29 +13084,25 @@ }, "node_modules/jest-each/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-each/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-each/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-each/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14595,8 +13112,7 @@ }, "node_modules/jest-environment-jsdom": { "version": "26.6.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.1.tgz", - "integrity": "sha512-A17RiXuHYNVlkM+3QNcQ6n5EZyAc6eld8ra9TW26luounGWpku4tj03uqRgHJCI1d4uHr5rJiuCH5JFRtdmrcA==", + "license": "MIT", "dependencies": { "@jest/environment": "^26.6.1", "@jest/fake-timers": "^26.6.1", @@ -14612,8 +13128,7 @@ }, "node_modules/jest-environment-node": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", - "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "license": "MIT", "dependencies": { "@jest/environment": "^26.6.2", "@jest/fake-timers": "^26.6.2", @@ -14628,17 +13143,15 @@ }, "node_modules/jest-get-type": { "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", - "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "@types/graceful-fs": "^4.1.2", @@ -14663,8 +13176,7 @@ }, "node_modules/jest-jasmine2": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", - "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "license": "MIT", "dependencies": { "@babel/traverse": "^7.1.0", "@jest/environment": "^26.6.2", @@ -14691,8 +13203,7 @@ }, "node_modules/jest-jasmine2/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -14705,8 +13216,7 @@ }, "node_modules/jest-jasmine2/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14720,8 +13230,7 @@ }, "node_modules/jest-jasmine2/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -14731,21 +13240,18 @@ }, "node_modules/jest-jasmine2/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-jasmine2/node_modules/diff-sequences": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-jasmine2/node_modules/expect": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "ansi-styles": "^4.0.0", @@ -14760,16 +13266,14 @@ }, "node_modules/jest-jasmine2/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-jasmine2/node_modules/jest-diff": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -14782,16 +13286,14 @@ }, "node_modules/jest-jasmine2/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-jasmine2/node_modules/jest-matcher-utils": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^26.6.2", @@ -14804,8 +13306,7 @@ }, "node_modules/jest-jasmine2/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -14823,16 +13324,14 @@ }, "node_modules/jest-jasmine2/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-jasmine2/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14842,8 +13341,7 @@ }, "node_modules/jest-leak-detector": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", - "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "license": "MIT", "dependencies": { "jest-get-type": "^26.3.0", "pretty-format": "^26.6.2" @@ -14854,26 +13352,23 @@ }, "node_modules/jest-leak-detector/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-localstorage-mock": { "version": "2.4.22", - "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.4.22.tgz", - "integrity": "sha512-60PWSDFQOS5v7JzSmYLM3dPLg0JLl+2Vc4lIEz/rj2yrXJzegsFLn7anwc5IL0WzJbBa/Las064CHbFg491/DQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=6.16.0" } }, "node_modules/jest-matcher-utils": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.5.0", @@ -14886,9 +13381,8 @@ }, "node_modules/jest-matcher-utils/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -14901,9 +13395,8 @@ }, "node_modules/jest-matcher-utils/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14917,9 +13410,8 @@ }, "node_modules/jest-matcher-utils/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -14929,24 +13421,21 @@ }, "node_modules/jest-matcher-utils/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-matcher-utils/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", @@ -14958,9 +13447,8 @@ }, "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -14970,15 +13458,13 @@ }, "node_modules/jest-matcher-utils/node_modules/react-is": { "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14988,9 +13474,8 @@ }, "node_modules/jest-message-util": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.5.0", @@ -15008,9 +13493,8 @@ }, "node_modules/jest-message-util/node_modules/@jest/types": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.4.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -15025,18 +13509,16 @@ }, "node_modules/jest-message-util/node_modules/@types/yargs": { "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/jest-message-util/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15049,9 +13531,8 @@ }, "node_modules/jest-message-util/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15065,9 +13546,8 @@ }, "node_modules/jest-message-util/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15077,24 +13557,21 @@ }, "node_modules/jest-message-util/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-message-util/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-message-util/node_modules/pretty-format": { "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", @@ -15106,9 +13583,8 @@ }, "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -15118,24 +13594,21 @@ }, "node_modules/jest-message-util/node_modules/react-is": { "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-message-util/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-message-util/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -15145,8 +13618,7 @@ }, "node_modules/jest-mock": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", - "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "@types/node": "*" @@ -15157,8 +13629,7 @@ }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "license": "MIT", "engines": { "node": ">=6" }, @@ -15173,16 +13644,14 @@ }, "node_modules/jest-regex-util": { "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-resolve": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", - "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "chalk": "^4.0.0", @@ -15199,8 +13668,7 @@ }, "node_modules/jest-resolve-dependencies": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", - "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "jest-regex-util": "^26.0.0", @@ -15212,8 +13680,7 @@ }, "node_modules/jest-resolve/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15226,8 +13693,7 @@ }, "node_modules/jest-resolve/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15241,8 +13707,7 @@ }, "node_modules/jest-resolve/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15252,29 +13717,25 @@ }, "node_modules/jest-resolve/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-resolve/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-resolve/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-resolve/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -15284,8 +13745,7 @@ }, "node_modules/jest-runner": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", - "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "license": "MIT", "dependencies": { "@jest/console": "^26.6.2", "@jest/environment": "^26.6.2", @@ -15314,8 +13774,7 @@ }, "node_modules/jest-runner/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15328,8 +13787,7 @@ }, "node_modules/jest-runner/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15343,8 +13801,7 @@ }, "node_modules/jest-runner/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15354,21 +13811,18 @@ }, "node_modules/jest-runner/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-runner/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-runner/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -15386,16 +13840,14 @@ }, "node_modules/jest-runner/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-runner/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -15405,8 +13857,7 @@ }, "node_modules/jest-runtime": { "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", - "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", + "license": "MIT", "dependencies": { "@jest/console": "^26.6.2", "@jest/environment": "^26.6.2", @@ -15445,8 +13896,7 @@ }, "node_modules/jest-runtime/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15459,16 +13909,14 @@ }, "node_modules/jest-runtime/node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/jest-runtime/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15482,8 +13930,7 @@ }, "node_modules/jest-runtime/node_modules/cliui": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -15492,8 +13939,7 @@ }, "node_modules/jest-runtime/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15503,13 +13949,11 @@ }, "node_modules/jest-runtime/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-runtime/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -15520,16 +13964,14 @@ }, "node_modules/jest-runtime/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-runtime/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -15547,8 +13989,7 @@ }, "node_modules/jest-runtime/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -15558,8 +13999,7 @@ }, "node_modules/jest-runtime/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -15572,8 +14012,7 @@ }, "node_modules/jest-runtime/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -15583,16 +14022,14 @@ }, "node_modules/jest-runtime/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-runtime/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -15602,8 +14039,7 @@ }, "node_modules/jest-runtime/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -15615,13 +14051,11 @@ }, "node_modules/jest-runtime/node_modules/y18n": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + "license": "ISC" }, "node_modules/jest-runtime/node_modules/yargs": { "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -15641,8 +14075,7 @@ }, "node_modules/jest-runtime/node_modules/yargs-parser": { "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -15653,8 +14086,7 @@ }, "node_modules/jest-serializer": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", - "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "license": "MIT", "dependencies": { "@types/node": "*", "graceful-fs": "^4.2.4" @@ -15665,8 +14097,7 @@ }, "node_modules/jest-snapshot": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", - "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0", "@jest/types": "^26.6.2", @@ -15691,8 +14122,7 @@ }, "node_modules/jest-snapshot/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15705,8 +14135,7 @@ }, "node_modules/jest-snapshot/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15720,8 +14149,7 @@ }, "node_modules/jest-snapshot/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15731,21 +14159,18 @@ }, "node_modules/jest-snapshot/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-snapshot/node_modules/diff-sequences": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-snapshot/node_modules/expect": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "ansi-styles": "^4.0.0", @@ -15760,16 +14185,14 @@ }, "node_modules/jest-snapshot/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-snapshot/node_modules/jest-diff": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -15782,16 +14205,14 @@ }, "node_modules/jest-snapshot/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^26.6.2", @@ -15804,8 +14225,7 @@ }, "node_modules/jest-snapshot/node_modules/jest-message-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -15823,8 +14243,7 @@ }, "node_modules/jest-snapshot/node_modules/semver": { "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -15837,16 +14256,14 @@ }, "node_modules/jest-snapshot/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -15856,8 +14273,7 @@ }, "node_modules/jest-util": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "@types/node": "*", @@ -15872,8 +14288,7 @@ }, "node_modules/jest-util/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15886,8 +14301,7 @@ }, "node_modules/jest-util/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15901,13 +14315,11 @@ }, "node_modules/jest-util/node_modules/ci-info": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + "license": "MIT" }, "node_modules/jest-util/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15917,21 +14329,18 @@ }, "node_modules/jest-util/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-util/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-util/node_modules/is-ci": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "license": "MIT", "dependencies": { "ci-info": "^2.0.0" }, @@ -15941,8 +14350,7 @@ }, "node_modules/jest-util/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -15952,8 +14360,7 @@ }, "node_modules/jest-validate": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", - "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "camelcase": "^6.0.0", @@ -15968,8 +14375,7 @@ }, "node_modules/jest-validate/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15982,8 +14388,7 @@ }, "node_modules/jest-validate/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15997,8 +14402,7 @@ }, "node_modules/jest-validate/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -16008,29 +14412,25 @@ }, "node_modules/jest-validate/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-validate/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-validate/node_modules/jest-get-type": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "license": "MIT", "engines": { "node": ">= 10.14.2" } }, "node_modules/jest-validate/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -16040,8 +14440,7 @@ }, "node_modules/jest-watcher": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", - "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "license": "MIT", "dependencies": { "@jest/test-result": "^26.6.2", "@jest/types": "^26.6.2", @@ -16057,8 +14456,7 @@ }, "node_modules/jest-watcher/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -16071,8 +14469,7 @@ }, "node_modules/jest-watcher/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -16086,8 +14483,7 @@ }, "node_modules/jest-watcher/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -16097,21 +14493,18 @@ }, "node_modules/jest-watcher/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/jest-watcher/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-watcher/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -16121,8 +14514,7 @@ }, "node_modules/jest-worker": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -16134,16 +14526,14 @@ }, "node_modules/jest-worker/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-worker/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -16153,14 +14543,12 @@ }, "node_modules/jquery": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz", - "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==", + "license": "MIT", "peer": true }, "node_modules/js-sdsl": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" @@ -16168,13 +14556,11 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -16185,8 +14571,7 @@ }, "node_modules/jsdom": { "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "license": "MIT", "dependencies": { "abab": "^2.0.5", "acorn": "^8.2.4", @@ -16230,13 +14615,11 @@ }, "node_modules/jsdom/node_modules/parse5": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + "license": "MIT" }, "node_modules/jsesc": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -16246,13 +14629,11 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "license": "MIT" }, "node_modules/json-stable-stringify": { "version": "1.1.0", @@ -16274,8 +14655,7 @@ }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "license": "MIT" }, "node_modules/json-stable-stringify/node_modules/isarray": { "version": "2.0.5", @@ -16285,8 +14665,7 @@ }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -16296,8 +14675,7 @@ }, "node_modules/jsonfile": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -16316,8 +14694,7 @@ }, "node_modules/jsx-ast-utils": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", - "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "license": "MIT", "dependencies": { "array-includes": "^3.1.5", "object.assign": "^4.1.3" @@ -16328,18 +14705,15 @@ }, "node_modules/just-curry-it": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.2.1.tgz", - "integrity": "sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==" + "license": "MIT" }, "node_modules/jwt-decode": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", - "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + "license": "MIT" }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -16355,37 +14729,32 @@ }, "node_modules/kleur": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/klona": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/language-subtag-registry": { "version": "0.3.22", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", - "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "license": "MIT", "dependencies": { "language-subtag-registry": "~0.3.2" } }, "node_modules/launch-editor": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "license": "MIT", "dependencies": { "picocolors": "^1.0.0", "shell-quote": "^1.7.3" @@ -16393,24 +14762,21 @@ }, "node_modules/launch-editor/node_modules/shell-quote": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/leven": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -16421,37 +14787,32 @@ }, "node_modules/lie": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } }, "node_modules/lilconfig": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "license": "MIT" }, "node_modules/loader-runner": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", "engines": { "node": ">=6.11.5" } }, "node_modules/loader-utils": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -16463,24 +14824,20 @@ }, "node_modules/localforage": { "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" } }, "node_modules/localforage-memoryStorageDriver": { "version": "0.9.2", - "resolved": "https://registry.npmjs.org/localforage-memoryStorageDriver/-/localforage-memoryStorageDriver-0.9.2.tgz", - "integrity": "sha512-DRB4BkkW9o5HIetbsuvtcg98GP7J1JBRDyDMJK13hfr9QsNpnMW6UUWmU9c6bcRg99akR1mGZ/ubUV1Ek0fbpg==", "dependencies": { "localforage": ">=1.4.0" } }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -16493,71 +14850,58 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "license": "MIT" }, "node_modules/lodash.escape": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", - "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "license": "MIT" }, "node_modules/lodash.snakecase": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + "license": "MIT" }, "node_modules/lodash.uniq": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + "license": "MIT" }, "node_modules/lodash.uniqby": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==" + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -16567,16 +14911,14 @@ }, "node_modules/lower-case": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } }, "node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -16586,16 +14928,14 @@ }, "node_modules/lz-string": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", "bin": { "lz-string": "bin/bin.js" } }, "node_modules/mailto-link": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-2.0.0.tgz", - "integrity": "sha512-b5FErkZ4t6mpH1IFZSw7Mm2IQHXQ2R0/5Q4xd7Rv8dVkWvE54mFG/UW7HjfFazXFjXTNsM+dSX2tTeIDrV9K9A==", + "license": "MIT", "dependencies": { "assert-ok": "~1.0.0", "cast-array": "~1.0.1", @@ -16608,8 +14948,7 @@ }, "node_modules/make-dir": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" @@ -16620,37 +14959,32 @@ }, "node_modules/make-dir/node_modules/semver": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/make-error": { "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + "license": "ISC" }, "node_modules/makeerror": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" } }, "node_modules/map-cache": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/map-visit": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "license": "MIT", "dependencies": { "object-visit": "^1.0.0" }, @@ -16660,16 +14994,14 @@ }, "node_modules/matchmediaquery": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz", - "integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==", + "license": "MIT", "dependencies": { "css-mediaquery": "^0.1.2" } }, "node_modules/mdast-util-definitions": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", - "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", + "license": "MIT", "dependencies": { "unist-util-visit": "^2.0.0" }, @@ -16680,8 +15012,7 @@ }, "node_modules/mdast-util-from-markdown": { "version": "0.8.5", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", - "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-string": "^2.0.0", @@ -16696,8 +15027,7 @@ }, "node_modules/mdast-util-to-hast": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz", - "integrity": "sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", @@ -16715,8 +15045,7 @@ }, "node_modules/mdast-util-to-string": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", - "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -16724,26 +15053,22 @@ }, "node_modules/mdn-data": { "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "license": "CC0-1.0" }, "node_modules/mdurl": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "license": "MIT" }, "node_modules/media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { "version": "3.4.13", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", - "integrity": "sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==", + "license": "Unlicense", "dependencies": { "fs-monkey": "^1.0.3" }, @@ -16753,40 +15078,33 @@ }, "node_modules/merge-descriptors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/methods": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/microevent.ts": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", - "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/micromark": { "version": "2.11.4", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", - "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", "funding": [ { "type": "GitHub Sponsors", @@ -16797,6 +15115,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "debug": "^4.0.0", "parse-entities": "^2.0.0" @@ -16804,8 +15123,7 @@ }, "node_modules/micromatch": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "license": "MIT", "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -16816,8 +15134,7 @@ }, "node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -16827,16 +15144,14 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -16846,16 +15161,14 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -16865,18 +15178,15 @@ }, "node_modules/min-indent": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/mini-create-react-context": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.1", "tiny-warning": "^1.0.3" @@ -16888,8 +15198,7 @@ }, "node_modules/mini-css-extract-plugin": { "version": "0.11.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.2.tgz", - "integrity": "sha512-h2LknfX4U1kScXxH8xE9LCOqT5B+068EAj36qicMb8l4dqdJoyHcmWmpd+ueyZfgu/POvIn+teoUnTtei2ikug==", + "license": "MIT", "peer": true, "dependencies": { "loader-utils": "^1.1.0", @@ -16910,8 +15219,7 @@ }, "node_modules/mini-css-extract-plugin/node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", "peer": true, "dependencies": { "minimist": "^1.2.0" @@ -16922,8 +15230,7 @@ }, "node_modules/mini-css-extract-plugin/node_modules/loader-utils": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "license": "MIT", "peer": true, "dependencies": { "big.js": "^5.2.2", @@ -16936,8 +15243,7 @@ }, "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "license": "MIT", "peer": true, "dependencies": { "ajv": "^6.1.0", @@ -16950,13 +15256,11 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "license": "ISC" }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -16966,16 +15270,14 @@ }, "node_modules/minimist": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mixin-deep": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "license": "MIT", "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -16986,47 +15288,40 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "license": "MIT" }, "node_modules/moo": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", - "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/moo-color": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", - "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "^1.1.4" } }, "node_modules/moo-color/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mrmime": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/ms": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "license": "MIT" }, "node_modules/multicast-dns": { "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -17037,14 +15332,13 @@ }, "node_modules/nanoid": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -17054,8 +15348,7 @@ }, "node_modules/nanomatch": { "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "license": "MIT", "dependencies": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -17075,24 +15368,20 @@ }, "node_modules/napi-build-utils": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "license": "MIT" }, "node_modules/natural-compare-lite": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" + "license": "MIT" }, "node_modules/nearley": { "version": "2.20.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", - "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", "dev": true, + "license": "MIT", "dependencies": { "commander": "^2.19.0", "moo": "^0.5.0", @@ -17112,32 +15401,27 @@ }, "node_modules/nearley/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/neo-async": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "license": "MIT" }, "node_modules/nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -17145,8 +15429,7 @@ }, "node_modules/node-abi": { "version": "3.43.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.43.0.tgz", - "integrity": "sha512-QB0MMv+tn9Ur2DtJrc8y09n0n6sw88CyDniWSX2cHW10goQXYPK9ZpFJOktDS4ron501edPX6h9i7Pg+RnH5nQ==", + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -17156,8 +15439,7 @@ }, "node_modules/node-abi/node_modules/semver": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -17170,26 +15452,22 @@ }, "node_modules/node-addon-api": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + "license": "MIT" }, "node_modules/node-forge": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-int64": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + "license": "MIT" }, "node_modules/node-notifier": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", - "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", + "license": "MIT", "optional": true, "dependencies": { "growly": "^1.3.0", @@ -17202,8 +15480,7 @@ }, "node_modules/node-notifier/node_modules/semver": { "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "license": "ISC", "optional": true, "dependencies": { "lru-cache": "^6.0.0" @@ -17217,8 +15494,7 @@ }, "node_modules/node-notifier/node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", "optional": true, "bin": { "uuid": "dist/bin/uuid" @@ -17226,13 +15502,11 @@ }, "node_modules/node-releases": { "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==" + "license": "MIT" }, "node_modules/normalize-package-data": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -17242,32 +15516,28 @@ }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-range": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-url": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "license": "MIT", "peer": true, "dependencies": { "object-assign": "^4.0.1", @@ -17281,8 +15551,7 @@ }, "node_modules/normalize-url/node_modules/query-string": { "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "license": "MIT", "peer": true, "dependencies": { "object-assign": "^4.1.0", @@ -17294,8 +15563,7 @@ }, "node_modules/npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "license": "MIT", "dependencies": { "path-key": "^2.0.0" }, @@ -17305,16 +15573,14 @@ }, "node_modules/npm-run-path/node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -17324,26 +15590,22 @@ }, "node_modules/nwsapi": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-code": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.2.4.tgz", - "integrity": "sha512-uGq4ETUuWe+GA586NXEriiaozNuff+YNFXlpD8cVrM1GoiuTZpCABP+bZCWDrvQDoCiSTyiWAFHD/HF/iwhb2w==" + "license": "MIT" }, "node_modules/object-copy": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "license": "MIT", "dependencies": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -17355,8 +15617,7 @@ }, "node_modules/object-copy/node_modules/define-property": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", "dependencies": { "is-descriptor": "^0.1.0" }, @@ -17366,8 +15627,7 @@ }, "node_modules/object-copy/node_modules/is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -17377,13 +15637,11 @@ }, "node_modules/object-copy/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/object-copy/node_modules/is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -17393,8 +15651,7 @@ }, "node_modules/object-copy/node_modules/is-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "license": "MIT", "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -17406,16 +15663,14 @@ }, "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-copy/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -17425,21 +15680,18 @@ }, "node_modules/object-filter": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", - "integrity": "sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==" + "license": "MIT" }, "node_modules/object-inspect": { "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-is": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -17453,16 +15705,14 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/object-visit": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "license": "MIT", "dependencies": { "isobject": "^3.0.0" }, @@ -17472,16 +15722,14 @@ }, "node_modules/object-visit/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object.assign": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -17497,8 +15745,7 @@ }, "node_modules/object.entries": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -17510,8 +15757,7 @@ }, "node_modules/object.fromentries": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -17526,8 +15772,7 @@ }, "node_modules/object.getownpropertydescriptors": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", - "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", + "license": "MIT", "peer": true, "dependencies": { "array.prototype.reduce": "^1.0.5", @@ -17545,8 +15790,7 @@ }, "node_modules/object.hasown": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "license": "MIT", "dependencies": { "define-properties": "^1.1.4", "es-abstract": "^1.20.4" @@ -17557,8 +15801,7 @@ }, "node_modules/object.pick": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -17568,16 +15811,14 @@ }, "node_modules/object.pick/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object.values": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -17592,13 +15833,11 @@ }, "node_modules/obuf": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -17608,24 +15847,21 @@ }, "node_modules/on-headers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -17638,9 +15874,8 @@ }, "node_modules/open": { "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -17654,16 +15889,14 @@ }, "node_modules/opener": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", "bin": { "opener": "bin/opener-bin.js" } }, "node_modules/optionator": { "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -17687,8 +15920,7 @@ }, "node_modules/p-each-series": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", - "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -17698,16 +15930,14 @@ }, "node_modules/p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -17720,8 +15950,7 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -17734,16 +15963,14 @@ }, "node_modules/p-map": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/p-retry": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -17754,16 +15981,14 @@ }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/param-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -17771,8 +15996,7 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -17782,8 +16006,7 @@ }, "node_modules/parse-entities": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -17799,8 +16022,7 @@ }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -17816,14 +16038,12 @@ }, "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==" + "license": "MIT" }, "node_modules/parse5": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.1.tgz", - "integrity": "sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==", "dev": true, + "license": "MIT", "dependencies": { "entities": "^4.4.0" }, @@ -17833,9 +16053,8 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", "dev": true, + "license": "MIT", "dependencies": { "domhandler": "^5.0.2", "parse5": "^7.0.0" @@ -17846,16 +16065,14 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/pascal-case": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -17863,8 +16080,7 @@ }, "node_modules/pascalcase": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -18010,69 +16226,59 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-is-inside": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" + "license": "(WTFPL OR MIT)" }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "license": "MIT", "dependencies": { "isarray": "0.0.1" } }, "node_modules/path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/performance-now": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -18082,24 +16288,21 @@ }, "node_modules/pify": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/pinkie": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/pinkie-promise": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "license": "MIT", "dependencies": { "pinkie": "^2.0.0" }, @@ -18109,16 +16312,14 @@ }, "node_modules/pirates": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/pkg-dir": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -18128,8 +16329,7 @@ }, "node_modules/pkg-dir/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -18140,8 +16340,7 @@ }, "node_modules/pkg-dir/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -18151,8 +16350,7 @@ }, "node_modules/pkg-dir/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -18165,8 +16363,7 @@ }, "node_modules/pkg-dir/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -18176,8 +16373,7 @@ }, "node_modules/pkg-up": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "license": "MIT", "dependencies": { "find-up": "^3.0.0" }, @@ -18187,8 +16383,7 @@ }, "node_modules/pkg-up/node_modules/find-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", "dependencies": { "locate-path": "^3.0.0" }, @@ -18198,8 +16393,7 @@ }, "node_modules/pkg-up/node_modules/locate-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -18210,8 +16404,7 @@ }, "node_modules/pkg-up/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -18224,8 +16417,7 @@ }, "node_modules/pkg-up/node_modules/p-locate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", "dependencies": { "p-limit": "^2.0.0" }, @@ -18235,17 +16427,14 @@ }, "node_modules/pkg-up/node_modules/path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/popper.js": { "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", "peer": true, "funding": { "type": "opencollective", @@ -18254,16 +16443,13 @@ }, "node_modules/posix-character-classes": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/postcss": { "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", "funding": [ { "type": "opencollective", @@ -18278,6 +16464,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -18289,8 +16476,7 @@ }, "node_modules/postcss-loader": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "license": "MIT", "dependencies": { "cosmiconfig": "^7.0.0", "klona": "^2.0.5", @@ -18310,8 +16496,7 @@ }, "node_modules/postcss-loader/node_modules/semver": { "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -18324,8 +16509,7 @@ }, "node_modules/postcss-modules-extract-imports": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -18335,8 +16519,7 @@ }, "node_modules/postcss-modules-local-by-default": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^6.0.2", @@ -18351,8 +16534,7 @@ }, "node_modules/postcss-modules-scope": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "license": "ISC", "dependencies": { "postcss-selector-parser": "^6.0.4" }, @@ -18365,8 +16547,7 @@ }, "node_modules/postcss-modules-values": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" }, @@ -18379,8 +16560,7 @@ }, "node_modules/postcss-rtlcss": { "version": "3.7.2", - "resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-3.7.2.tgz", - "integrity": "sha512-GurrGedCKvOTe1QrifI+XpDKXA3bJky1v8KiOa/TYYHs1bfJOxI53GIRvVSqLJLly7e1WcNMz8KMESTN01vbZQ==", + "license": "Apache-2.0", "dependencies": { "rtlcss": "^3.5.0" }, @@ -18393,8 +16573,7 @@ }, "node_modules/postcss-selector-parser": { "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -18405,13 +16584,11 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "license": "MIT" }, "node_modules/prebuild-install": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -18435,16 +16612,14 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prepend-http": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -18452,8 +16627,7 @@ }, "node_modules/pretty-error": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "license": "MIT", "peer": true, "dependencies": { "lodash": "^4.17.20", @@ -18462,8 +16636,7 @@ }, "node_modules/pretty-format": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "license": "MIT", "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", @@ -18476,8 +16649,7 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -18490,8 +16662,7 @@ }, "node_modules/pretty-format/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -18501,23 +16672,19 @@ }, "node_modules/pretty-format/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/pretty-format/node_modules/react-is": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "license": "MIT" }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "license": "MIT" }, "node_modules/prompts": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -18528,29 +16695,16 @@ }, "node_modules/prop-types": { "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.8.1" } }, - "node_modules/prop-types-exact": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", - "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", - "dev": true, - "dependencies": { - "has": "^1.0.3", - "object.assign": "^4.1.0", - "reflect.ownkeys": "^0.2.0" - } - }, "node_modules/prop-types-extra": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", - "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", "dependencies": { "react-is": "^16.3.2", "warning": "^4.0.0" @@ -18561,8 +16715,7 @@ }, "node_modules/property-information": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", "dependencies": { "xtend": "^4.0.0" }, @@ -18573,8 +16726,7 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -18585,26 +16737,22 @@ }, "node_modules/proxy-addr/node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/psl": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + "license": "MIT" }, "node_modules/pubsub-js": { "version": "1.9.4", - "resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.4.tgz", - "integrity": "sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A==" + "license": "MIT" }, "node_modules/pump": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -18612,16 +16760,14 @@ }, "node_modules/punycode": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/purgecss": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-5.0.0.tgz", - "integrity": "sha512-RAnuxrGuVyLLTr8uMbKaxDRGWMgK5CCYDfRyUNNcaz5P3kGgD2b7ymQGYEyo2ST7Tl/ScwFgf5l3slKMxHSbrw==", + "license": "MIT", "dependencies": { "commander": "^9.0.0", "glob": "^8.0.3", @@ -18634,24 +16780,21 @@ }, "node_modules/purgecss/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/purgecss/node_modules/commander": { "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", "engines": { "node": "^12.20.0 || >=14" } }, "node_modules/purgecss/node_modules/glob": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -18668,8 +16811,7 @@ }, "node_modules/purgecss/node_modules/minimatch": { "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -18679,8 +16821,7 @@ }, "node_modules/qs": { "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.4" }, @@ -18693,8 +16834,7 @@ }, "node_modules/query-string": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz", - "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==", + "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.0", "filter-obj": "^1.1.0", @@ -18710,30 +16850,23 @@ }, "node_modules/query-string/node_modules/strict-uri-encode": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/querystring": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", "engines": { "node": ">=0.4.x" } }, "node_modules/querystringify": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -18747,33 +16880,30 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/raf": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "dev": true, + "license": "MIT", "dependencies": { "performance-now": "^2.1.0" } }, "node_modules/railroad-diagrams": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/ramda": { "version": "0.26.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", - "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==" + "license": "MIT" }, "node_modules/randexp": { "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", "dev": true, + "license": "MIT", "dependencies": { "discontinuous-range": "1.0.0", "ret": "~0.1.10" @@ -18784,24 +16914,21 @@ }, "node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -18814,16 +16941,14 @@ }, "node_modules/raw-body/node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -18836,20 +16961,17 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "version": "17.0.2", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" }, "engines": { "node": ">=0.10.0" @@ -18857,8 +16979,7 @@ }, "node_modules/react-bootstrap": { "version": "1.6.6", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.6.tgz", - "integrity": "sha512-pSzYyJT5u4rc8+5myM8Vid2JG52L8AmYSkpznReH/GM4+FhLqEnxUa0+6HRTaGwjdEixQNGchwY+b3xCdYWrDA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", @@ -18885,13 +17006,11 @@ }, "node_modules/react-bootstrap/node_modules/classnames": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + "license": "MIT" }, "node_modules/react-clientside-effect": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", - "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.13" }, @@ -18901,8 +17020,7 @@ }, "node_modules/react-colorful": { "version": "5.6.1", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", - "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" @@ -18910,9 +17028,8 @@ }, "node_modules/react-dev-utils": { "version": "11.0.4", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", - "integrity": "sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "7.10.4", "address": "1.1.2", @@ -18945,27 +17062,24 @@ }, "node_modules/react-dev-utils/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/react-dev-utils/node_modules/array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/react-dev-utils/node_modules/browserslist": { "version": "4.14.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.2.tgz", - "integrity": "sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw==", "dev": true, + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001125", "electron-to-chromium": "^1.3.564", @@ -18985,18 +17099,16 @@ }, "node_modules/react-dev-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/react-dev-utils/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -19007,9 +17119,8 @@ }, "node_modules/react-dev-utils/node_modules/globby": { "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -19027,9 +17138,8 @@ }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", "dev": true, + "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -19041,9 +17151,8 @@ }, "node_modules/react-dev-utils/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -19053,15 +17162,13 @@ }, "node_modules/react-dev-utils/node_modules/node-releases": { "version": "1.1.77", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", - "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/react-dev-utils/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -19074,9 +17181,8 @@ }, "node_modules/react-dev-utils/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -19086,9 +17192,8 @@ }, "node_modules/react-dev-utils/node_modules/prompts": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", - "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", "dev": true, + "license": "MIT", "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -19099,18 +17204,16 @@ }, "node_modules/react-dev-utils/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/react-dev-utils/node_modules/strip-ansi": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.0" }, @@ -19119,23 +17222,20 @@ } }, "node_modules/react-dom": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", - "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "version": "17.0.2", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.20.2" }, "peerDependencies": { - "react": "^16.13.1" + "react": "17.0.2" } }, "node_modules/react-dom/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.20.2", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -19143,8 +17243,7 @@ }, "node_modules/react-dropzone": { "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "license": "MIT", "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.6.0", @@ -19159,8 +17258,7 @@ }, "node_modules/react-dropzone/node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -19169,9 +17267,8 @@ }, "node_modules/react-error-boundary": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", - "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -19185,18 +17282,15 @@ }, "node_modules/react-error-overlay": { "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" + "license": "MIT" }, "node_modules/react-fast-compare": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", - "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + "version": "3.2.2", + "license": "MIT" }, "node_modules/react-focus-lock": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.2.tgz", - "integrity": "sha512-5JfrsOKyA5Zn3h958mk7bAcfphr24jPoMoznJ8vaJF6fUrPQ8zrtEd3ILLOK8P5jvGxdMd96OxWNjDzATfR2qw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.0.0", "focus-lock": "^0.11.2", @@ -19217,8 +17311,7 @@ }, "node_modules/react-focus-on": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.7.0.tgz", - "integrity": "sha512-TsCnbJr4qjqFatJ4U1N8qGSZH+FUzxJ5mJ5ta7TY2YnDmUbGGmcvZMTZgGjQ1fl6vlztsMyg6YyZlPAeeIhEUg==", + "license": "MIT", "dependencies": { "aria-hidden": "^1.2.2", "react-focus-lock": "^2.9.2", @@ -19242,23 +17335,21 @@ } }, "node_modules/react-helmet": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz", - "integrity": "sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==", + "version": "6.1.0", + "license": "MIT", "dependencies": { "object-assign": "^4.1.1", - "prop-types": "^15.5.4", - "react-fast-compare": "^2.0.2", - "react-side-effect": "^1.1.0" + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" }, "peerDependencies": { - "react": ">=15.0.0" + "react": ">=16.3.0" } }, "node_modules/react-instantsearch-core": { "version": "6.38.1", - "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.38.1.tgz", - "integrity": "sha512-14gy/jsakJELVeMEO+QmsHcugIyaU1pRyyuQjuXuBvF+TMHiWUjfYw7de3Lc4oYcTYIeSllYIxLHxdUoxLWZaA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", "algoliasearch-helper": "^3.11.1", @@ -19270,15 +17361,9 @@ "react": ">= 16.3.0 < 19" } }, - "node_modules/react-instantsearch-core/node_modules/react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, "node_modules/react-instantsearch-dom": { "version": "6.8.3", - "resolved": "https://registry.npmjs.org/react-instantsearch-dom/-/react-instantsearch-dom-6.8.3.tgz", - "integrity": "sha512-+Z0zgARvnfDbMnPIZX8CfwKE+TCX7E751Ty4RDnjnmpgnrfYzoodMr4/By/3vZ2KRk308gpyx9yyXZ18/3Ay/g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", "algoliasearch-helper": "^3.1.0", @@ -19292,15 +17377,9 @@ "react-dom": ">= 16.3.0 < 18" } }, - "node_modules/react-instantsearch-dom/node_modules/react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, "node_modules/react-intl": { "version": "5.25.1", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz", - "integrity": "sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==", + "license": "BSD-3-Clause", "dependencies": { "@formatjs/ecma402-abstract": "1.11.4", "@formatjs/icu-messageformat-parser": "2.1.0", @@ -19325,26 +17404,22 @@ }, "node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + "license": "MIT" }, "node_modules/react-loading-skeleton": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.0.tgz", - "integrity": "sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/react-markdown": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-6.0.0.tgz", - "integrity": "sha512-MC+zljUJeoLb4RbDm/wRbfoQFEZGz4TDOt/wb4dEehdaJWxLMn/T2IgwhQy0VYhuPEd2fhd7iOayE8lmENU0FA==", + "license": "MIT", "dependencies": { "@types/hast": "^2.0.0", "@types/unist": "^2.0.3", @@ -19370,13 +17445,11 @@ }, "node_modules/react-markdown/node_modules/react-is": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "license": "MIT" }, "node_modules/react-overlays": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.8", "@popperjs/core": "^2.11.6", @@ -19394,8 +17467,7 @@ }, "node_modules/react-popper": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", - "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "license": "MIT", "dependencies": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -19406,50 +17478,51 @@ "react-dom": "^16.8.0 || ^17 || ^18" } }, - "node_modules/react-popper/node_modules/react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, "node_modules/react-property": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", - "integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==" + "license": "MIT" }, "node_modules/react-proptype-conditional-require": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz", - "integrity": "sha512-nopsRn7KnGgazBe2c3H2+Kf+Csp6PGDRLiBkYEDMKY8o/EIgft/WnIm/OnAKTawZiLnJXHAqhpFBddvs6NiXlw==" + "license": "MIT" }, "node_modules/react-redux": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.1.tgz", - "integrity": "sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg==", + "version": "7.2.9", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.5.5", - "hoist-non-react-statics": "^3.3.0", - "invariant": "^2.2.4", + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", "loose-envify": "^1.4.0", "prop-types": "^15.7.2", - "react-is": "^16.9.0" + "react-is": "^17.0.2" }, "peerDependencies": { - "react": "^16.8.3", - "redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0" + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } } }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-remove-scroll": { "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", @@ -19472,8 +17545,7 @@ }, "node_modules/react-remove-scroll-bar": { "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -19493,8 +17565,7 @@ }, "node_modules/react-responsive": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz", - "integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==", + "license": "MIT", "dependencies": { "hyphenate-style-name": "^1.0.0", "matchmediaquery": "^0.3.0", @@ -19510,8 +17581,7 @@ }, "node_modules/react-router": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", - "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", @@ -19530,8 +17600,7 @@ }, "node_modules/react-router-dom": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", - "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", @@ -19545,21 +17614,28 @@ "react": ">=15" } }, - "node_modules/react-side-effect": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz", - "integrity": "sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==", + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "dev": true, + "license": "MIT", "dependencies": { - "shallowequal": "^1.0.1" + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" }, "peerDependencies": { - "react": "^0.13.0 || ^0.14.0 || ^15.0.0 || ^16.0.0" + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-style-singleton": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", "invariant": "^2.2.4", @@ -19580,8 +17656,7 @@ }, "node_modules/react-table": { "version": "7.8.0", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", - "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -19591,25 +17666,28 @@ } }, "node_modules/react-test-renderer": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz", - "integrity": "sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==", + "version": "17.0.2", "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "react-is": "^16.8.6", - "scheduler": "^0.19.1" + "react-is": "^17.0.2", + "react-shallow-renderer": "^16.13.1", + "scheduler": "^0.20.2" }, "peerDependencies": { - "react": "^16.13.1" + "react": "17.0.2" } }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT" + }, "node_modules/react-test-renderer/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.20.2", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -19617,8 +17695,7 @@ }, "node_modules/react-transition-group": { "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -19630,19 +17707,9 @@ "react-dom": ">=16.6.0" } }, - "node_modules/react-truncate": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-truncate/-/react-truncate-2.4.0.tgz", - "integrity": "sha512-3QW11/COYwi6iPUaunUhl06DW5NJBJD1WkmxW5YxqqUu6kvP+msB3jfoLg8WRbu57JqgebjVW8Lknw6T5/QZdA==", - "peerDependencies": { - "prop-types": "<= 15.x.x", - "react": "<= 16.x.x" - } - }, "node_modules/read-pkg": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", @@ -19655,8 +17722,7 @@ }, "node_modules/read-pkg-up": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "license": "MIT", "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", @@ -19671,8 +17737,7 @@ }, "node_modules/read-pkg-up/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -19683,8 +17748,7 @@ }, "node_modules/read-pkg-up/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -19694,8 +17758,7 @@ }, "node_modules/read-pkg-up/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -19708,8 +17771,7 @@ }, "node_modules/read-pkg-up/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -19719,24 +17781,21 @@ }, "node_modules/read-pkg-up/node_modules/type-fest": { "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" } }, "node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" } }, "node_modules/readable-stream": { "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -19749,18 +17808,15 @@ }, "node_modules/readable-stream/node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "license": "MIT" }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -19770,8 +17826,7 @@ }, "node_modules/rechoir": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "license": "MIT", "dependencies": { "resolve": "^1.20.0" }, @@ -19781,8 +17836,7 @@ }, "node_modules/recursive-readdir": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "license": "MIT", "dependencies": { "minimatch": "3.0.4" }, @@ -19792,8 +17846,7 @@ }, "node_modules/recursive-readdir/node_modules/minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -19803,9 +17856,8 @@ }, "node_modules/redent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, + "license": "MIT", "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -19816,9 +17868,8 @@ }, "node_modules/redent/node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, + "license": "MIT", "dependencies": { "min-indent": "^1.0.0" }, @@ -19828,13 +17879,11 @@ }, "node_modules/reduce-reducers": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-0.4.3.tgz", - "integrity": "sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==" + "license": "MIT" }, "node_modules/redux": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", - "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "symbol-observable": "^1.2.0" @@ -19842,8 +17891,7 @@ }, "node_modules/redux-actions": { "version": "2.6.5", - "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-2.6.5.tgz", - "integrity": "sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw==", + "license": "MIT", "dependencies": { "invariant": "^2.2.4", "just-curry-it": "^3.1.0", @@ -19854,17 +17902,14 @@ }, "node_modules/redux-devtools-extension": { "version": "2.13.8", - "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz", - "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==", - "deprecated": "Package moved to @redux-devtools/extension.", + "license": "MIT", "peerDependencies": { "redux": "^3.1.0 || ^4.0.0" } }, "node_modules/redux-form": { "version": "8.3.8", - "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-8.3.8.tgz", - "integrity": "sha512-PzXhA0d+awIc4PkuhbDa6dCEiraMrGMyyDlYEVNX6qEyW/G2SqZXrjav5zrpXb0CCeqQSc9iqwbMtYQXbJbOAQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.9.2", "es6-error": "^4.1.1", @@ -19896,40 +17941,29 @@ }, "node_modules/redux-logger": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", - "integrity": "sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==", + "license": "MIT", "dependencies": { "deep-diff": "^0.3.5" } }, "node_modules/redux-mock-store": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", - "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "license": "MIT", "dependencies": { "lodash.isplainobject": "^4.0.6" } }, "node_modules/redux-thunk": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", - "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" - }, - "node_modules/reflect.ownkeys": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", - "integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg==", - "dev": true + "license": "MIT" }, "node_modules/regenerate": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -19939,21 +17973,18 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + "license": "MIT" }, "node_modules/regenerator-transform": { "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/regex-not": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "license": "MIT", "dependencies": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" @@ -19964,13 +17995,11 @@ }, "node_modules/regex-parser": { "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" + "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -19985,8 +18014,7 @@ }, "node_modules/regexpu-core": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "license": "MIT", "dependencies": { "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", @@ -20001,8 +18029,7 @@ }, "node_modules/regjsparser": { "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "license": "BSD-2-Clause", "dependencies": { "jsesc": "~0.5.0" }, @@ -20012,24 +18039,20 @@ }, "node_modules/regjsparser/node_modules/jsesc": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", "bin": { "jsesc": "bin/jsesc" } }, "node_modules/relateurl": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/remark-parse": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", - "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^0.8.0" }, @@ -20040,8 +18063,7 @@ }, "node_modules/remark-rehype": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-8.1.0.tgz", - "integrity": "sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==", + "license": "MIT", "dependencies": { "mdast-util-to-hast": "^10.2.0" }, @@ -20052,18 +18074,15 @@ }, "node_modules/remove-accents": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", - "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + "license": "MIT" }, "node_modules/remove-trailing-separator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==" + "license": "ISC" }, "node_modules/renderkid": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", - "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "license": "MIT", "peer": true, "dependencies": { "css-select": "^4.1.3", @@ -20075,8 +18094,7 @@ }, "node_modules/renderkid/node_modules/ansi-regex": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -20084,8 +18102,7 @@ }, "node_modules/renderkid/node_modules/css-select": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", "peer": true, "dependencies": { "boolbase": "^1.0.0", @@ -20100,8 +18117,7 @@ }, "node_modules/renderkid/node_modules/dom-serializer": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "peer": true, "dependencies": { "domelementtype": "^2.0.1", @@ -20114,8 +18130,7 @@ }, "node_modules/renderkid/node_modules/domhandler": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "peer": true, "dependencies": { "domelementtype": "^2.2.0" @@ -20129,8 +18144,7 @@ }, "node_modules/renderkid/node_modules/domutils": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "peer": true, "dependencies": { "dom-serializer": "^1.0.1", @@ -20143,8 +18157,7 @@ }, "node_modules/renderkid/node_modules/entities": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", "peer": true, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -20152,8 +18165,6 @@ }, "node_modules/renderkid/node_modules/htmlparser2": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -20161,6 +18172,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "peer": true, "dependencies": { "domelementtype": "^2.0.1", @@ -20171,8 +18183,7 @@ }, "node_modules/renderkid/node_modules/strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", "peer": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -20183,56 +18194,48 @@ }, "node_modules/repeat-element": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/repeat-string": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", "engines": { "node": ">=0.10" } }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/require-main-filename": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + "license": "ISC" }, "node_modules/requires-port": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + "license": "MIT" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/resolve": { "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "license": "MIT", "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -20247,8 +18250,7 @@ }, "node_modules/resolve-cwd": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -20258,27 +18260,22 @@ }, "node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/resolve-pathname": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + "license": "MIT" }, "node_modules/resolve-url": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated" + "license": "MIT" }, "node_modules/resolve-url-loader": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", - "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "license": "MIT", "dependencies": { "adjust-sourcemap-loader": "^4.0.0", "convert-source-map": "^1.7.0", @@ -20292,32 +18289,28 @@ }, "node_modules/resolve-url-loader/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/ret": { "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "license": "MIT", "engines": { "node": ">=0.12" } }, "node_modules/retry": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/reusify": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -20325,8 +18318,7 @@ }, "node_modules/rimraf": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -20336,9 +18328,8 @@ }, "node_modules/rst-selector-parser": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", - "integrity": "sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "lodash.flattendeep": "^4.4.0", "nearley": "^2.7.10" @@ -20346,16 +18337,14 @@ }, "node_modules/rsvp": { "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "license": "MIT", "engines": { "node": "6.* || >= 7.*" } }, "node_modules/rtlcss": { "version": "3.5.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz", - "integrity": "sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==", + "license": "MIT", "dependencies": { "find-up": "^5.0.0", "picocolors": "^1.0.0", @@ -20368,8 +18357,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -20384,14 +18371,14 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-array-concat": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", - "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "license": "MIT", "peer": true, "dependencies": { "call-bind": "^1.0.2", @@ -20408,14 +18395,11 @@ }, "node_modules/safe-array-concat/node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT", "peer": true }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -20429,20 +18413,19 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "license": "MIT", "dependencies": { "ret": "~0.1.10" } }, "node_modules/safe-regex-test": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -20454,14 +18437,11 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "license": "MIT" }, "node_modules/sane": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", + "license": "MIT", "dependencies": { "@cnakazawa/watch": "^1.0.3", "anymatch": "^2.0.0", @@ -20482,8 +18462,7 @@ }, "node_modules/sane/node_modules/anymatch": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "license": "ISC", "dependencies": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" @@ -20491,8 +18470,7 @@ }, "node_modules/sane/node_modules/braces": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "license": "MIT", "dependencies": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -20511,8 +18489,7 @@ }, "node_modules/sane/node_modules/braces/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -20522,8 +18499,7 @@ }, "node_modules/sane/node_modules/fill-range": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "license": "MIT", "dependencies": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -20536,8 +18512,7 @@ }, "node_modules/sane/node_modules/fill-range/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -20547,21 +18522,18 @@ }, "node_modules/sane/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/sane/node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/sane/node_modules/is-number": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -20571,8 +18543,7 @@ }, "node_modules/sane/node_modules/is-number/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -20582,16 +18553,14 @@ }, "node_modules/sane/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/sane/node_modules/micromatch": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "license": "MIT", "dependencies": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -20613,8 +18582,7 @@ }, "node_modules/sane/node_modules/normalize-path": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", "dependencies": { "remove-trailing-separator": "^1.0.1" }, @@ -20624,8 +18592,7 @@ }, "node_modules/sane/node_modules/to-regex-range": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "license": "MIT", "dependencies": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -20636,8 +18603,7 @@ }, "node_modules/sanitize-html": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.8.1.tgz", - "integrity": "sha512-qK5neD0SaMxGwVv5txOYv05huC3o6ZAA4h5+7nJJgWMNFUNRjcjLO6FpwAtKzfKCZ0jrG6xTk6eVFskbvOGblg==", + "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -20649,8 +18615,7 @@ }, "node_modules/sanitize-html/node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -20660,16 +18625,14 @@ }, "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==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/sass": { "version": "1.62.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz", - "integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==", + "license": "MIT", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -20684,8 +18647,7 @@ }, "node_modules/sass-loader": { "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "license": "MIT", "dependencies": { "klona": "^2.0.4", "neo-async": "^2.6.2" @@ -20721,8 +18683,7 @@ }, "node_modules/saxes": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" }, @@ -20732,16 +18693,14 @@ }, "node_modules/scheduler": { "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -20757,13 +18716,11 @@ }, "node_modules/select-hose": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" + "license": "MIT" }, "node_modules/selfsigned": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "license": "MIT", "dependencies": { "node-forge": "^1" }, @@ -20773,16 +18730,14 @@ }, "node_modules/semver": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/send": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -20804,34 +18759,29 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/send/node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "license": "MIT" }, "node_modules/serialize-javascript": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, "node_modules/serve-index": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "license": "MIT", "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", @@ -20847,24 +18797,21 @@ }, "node_modules/serve-index/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/serve-index/node_modules/depd": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -20877,31 +18824,26 @@ }, "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + "license": "ISC" }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + "license": "ISC" }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/serve-static": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "license": "MIT", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -20914,8 +18856,7 @@ }, "node_modules/set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "license": "ISC" }, "node_modules/set-function-length": { "version": "1.1.1", @@ -20933,8 +18874,7 @@ }, "node_modules/set-value": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "license": "MIT", "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -20947,8 +18887,7 @@ }, "node_modules/set-value/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -20958,21 +18897,18 @@ }, "node_modules/set-value/node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -20982,19 +18918,12 @@ }, "node_modules/shallow-equal": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", - "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "license": "MIT" }, "node_modules/sharp": { "version": "0.32.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.1.tgz", - "integrity": "sha512-kQTFtj7ldpUqSe8kDxoGLZc1rnMFU0AO2pqbX6pLy3b7Oj8ivJIdoKNwxHVQG2HN6XpHPJqCSM2nsma2gOXvOg==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.1", @@ -21014,8 +18943,7 @@ }, "node_modules/sharp/node_modules/color": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -21026,8 +18954,7 @@ }, "node_modules/sharp/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -21037,13 +18964,11 @@ }, "node_modules/sharp/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/sharp/node_modules/semver": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -21056,8 +18981,7 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -21067,28 +18991,24 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/shellwords": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "license": "MIT", "optional": true }, "node_modules/side-channel": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -21100,13 +19020,10 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "license": "ISC" }, "node_modules/simple-concat": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "funding": [ { "type": "github", @@ -21120,12 +19037,11 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/simple-get": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "funding": [ { "type": "github", @@ -21140,6 +19056,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -21148,21 +19065,18 @@ }, "node_modules/simple-swizzle": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } }, "node_modules/simple-swizzle/node_modules/is-arrayish": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + "license": "MIT" }, "node_modules/sirv": { "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.20", "mrmime": "^1.0.0", @@ -21174,21 +19088,18 @@ }, "node_modules/sisteransi": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + "license": "MIT" }, "node_modules/slash": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/snapdragon": { "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "license": "MIT", "dependencies": { "base": "^0.11.1", "debug": "^2.2.0", @@ -21205,8 +19116,7 @@ }, "node_modules/snapdragon-node": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "license": "MIT", "dependencies": { "define-property": "^1.0.0", "isobject": "^3.0.0", @@ -21218,8 +19128,7 @@ }, "node_modules/snapdragon-node/node_modules/define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", "dependencies": { "is-descriptor": "^1.0.0" }, @@ -21229,16 +19138,14 @@ }, "node_modules/snapdragon-node/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-util": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "license": "MIT", "dependencies": { "kind-of": "^3.2.0" }, @@ -21248,13 +19155,11 @@ }, "node_modules/snapdragon-util/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/snapdragon-util/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -21264,16 +19169,14 @@ }, "node_modules/snapdragon/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/snapdragon/node_modules/define-property": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", "dependencies": { "is-descriptor": "^0.1.0" }, @@ -21283,8 +19186,7 @@ }, "node_modules/snapdragon/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -21294,8 +19196,7 @@ }, "node_modules/snapdragon/node_modules/is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -21305,8 +19206,7 @@ }, "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -21316,13 +19216,11 @@ }, "node_modules/snapdragon/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/snapdragon/node_modules/is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -21332,8 +19230,7 @@ }, "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -21343,8 +19240,7 @@ }, "node_modules/snapdragon/node_modules/is-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "license": "MIT", "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -21356,30 +19252,25 @@ }, "node_modules/snapdragon/node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon/node_modules/kind-of": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/snapdragon/node_modules/source-map-resolve": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "license": "MIT", "dependencies": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", @@ -21390,8 +19281,7 @@ }, "node_modules/sockjs": { "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", @@ -21400,16 +19290,14 @@ }, "node_modules/sockjs/node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/sort-keys": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "license": "MIT", "peer": true, "dependencies": { "is-plain-obj": "^1.0.0" @@ -21420,29 +19308,25 @@ }, "node_modules/source-list-map": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + "license": "MIT" }, "node_modules/source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-loader": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", - "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "license": "MIT", "dependencies": { "abab": "^2.0.6", "iconv-lite": "^0.6.3", @@ -21461,8 +19345,7 @@ }, "node_modules/source-map-loader/node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -21472,8 +19355,7 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -21481,22 +19363,18 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-url": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated" + "license": "MIT" }, "node_modules/space-separated-tokens": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -21504,8 +19382,7 @@ }, "node_modules/spdx-correct": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -21513,13 +19390,11 @@ }, "node_modules/spdx-exceptions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -21527,13 +19402,11 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" + "license": "CC0-1.0" }, "node_modules/spdy": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", @@ -21547,8 +19420,7 @@ }, "node_modules/spdy-transport": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", @@ -21560,8 +19432,7 @@ }, "node_modules/spdy-transport/node_modules/readable-stream": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -21573,16 +19444,14 @@ }, "node_modules/split-on-first": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/split-string": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "license": "MIT", "dependencies": { "extend-shallow": "^3.0.0" }, @@ -21592,19 +19461,15 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "license": "BSD-3-Clause" }, "node_modules/stable": { "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" + "license": "MIT" }, "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -21614,21 +19479,18 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/stackframe": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + "license": "MIT" }, "node_modules/static-extend": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "license": "MIT", "dependencies": { "define-property": "^0.2.5", "object-copy": "^0.1.0" @@ -21639,8 +19501,7 @@ }, "node_modules/static-extend/node_modules/define-property": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", "dependencies": { "is-descriptor": "^0.1.0" }, @@ -21650,8 +19511,7 @@ }, "node_modules/static-extend/node_modules/is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -21661,8 +19521,7 @@ }, "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -21672,13 +19531,11 @@ }, "node_modules/static-extend/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/static-extend/node_modules/is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -21688,8 +19545,7 @@ }, "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -21699,8 +19555,7 @@ }, "node_modules/static-extend/node_modules/is-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "license": "MIT", "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -21712,24 +19567,21 @@ }, "node_modules/static-extend/node_modules/kind-of": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "license": "MIT", "dependencies": { "internal-slot": "^1.0.4" }, @@ -21739,8 +19591,7 @@ }, "node_modules/strict-uri-encode": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -21748,21 +19599,18 @@ }, "node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "license": "MIT" }, "node_modules/string-length": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "license": "MIT", "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -21773,8 +19621,7 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -21786,13 +19633,11 @@ }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "license": "MIT" }, "node_modules/string.prototype.matchall": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -21809,8 +19654,7 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -21825,8 +19669,7 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -21838,8 +19681,7 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -21851,8 +19693,7 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -21862,41 +19703,36 @@ }, "node_modules/strip-bom": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/strip-final-newline": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/strip-indent": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -21906,8 +19742,7 @@ }, "node_modules/style-loader": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", - "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", + "license": "MIT", "engines": { "node": ">= 12.13.0" }, @@ -21921,33 +19756,28 @@ }, "node_modules/style-to-js": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.2.tgz", - "integrity": "sha512-aMG8jJpEF0SCGbQFY8W8CT+EjQ9ubp35FOZG3prWkNjxW/a1bEeSod0tkWiP+6iiOCDIIrQykUDkPY5LbNF87g==", + "license": "MIT", "dependencies": { "style-to-object": "0.4.0" } }, "node_modules/style-to-js/node_modules/style-to-object": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.0.tgz", - "integrity": "sha512-dAjq2m87tPn/TcYTeqMhXJRhu96WYWcxMFQxs3Y9jfYpq2jG+38u4tj0Lst6DOiYXmDuNxVJ2b1Z2uPC6wTEeg==", + "license": "MIT", "dependencies": { "inline-style-parser": "0.1.1" } }, "node_modules/style-to-object": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", - "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "license": "MIT", "dependencies": { "inline-style-parser": "0.1.1" } }, "node_modules/superagent": { "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at .", + "license": "MIT", "dependencies": { "component-emitter": "^1.2.0", "cookiejar": "^2.1.0", @@ -21966,16 +19796,14 @@ }, "node_modules/superagent/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/superagent/node_modules/form-data": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -21987,8 +19815,7 @@ }, "node_modules/superjson": { "version": "1.13.3", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", - "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "license": "MIT", "dependencies": { "copy-anything": "^3.0.2" }, @@ -21998,8 +19825,7 @@ }, "node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -22009,8 +19835,7 @@ }, "node_modules/supports-hyperlinks": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" @@ -22021,16 +19846,14 @@ }, "node_modules/supports-hyperlinks/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/supports-hyperlinks/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -22040,8 +19863,7 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -22051,13 +19873,11 @@ }, "node_modules/svg-parser": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + "license": "MIT" }, "node_modules/svgo": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "license": "MIT", "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", @@ -22076,16 +19896,14 @@ }, "node_modules/svgo/node_modules/commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/svgo/node_modules/css-select": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -22099,8 +19917,7 @@ }, "node_modules/svgo/node_modules/dom-serializer": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -22112,8 +19929,7 @@ }, "node_modules/svgo/node_modules/domhandler": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -22126,8 +19942,7 @@ }, "node_modules/svgo/node_modules/domutils": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -22139,42 +19954,36 @@ }, "node_modules/svgo/node_modules/entities": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/symbol-observable": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + "license": "MIT" }, "node_modules/tabbable": { "version": "5.3.3", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", - "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" + "license": "MIT" }, "node_modules/tapable": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/tar-fs": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -22184,8 +19993,7 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -22199,8 +20007,7 @@ }, "node_modules/tar-stream/node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -22212,8 +20019,7 @@ }, "node_modules/terminal-link": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "license": "MIT", "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" @@ -22227,8 +20033,7 @@ }, "node_modules/terser": { "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "license": "BSD-2-Clause", "peer": true, "dependencies": { "commander": "^2.20.0", @@ -22244,8 +20049,7 @@ }, "node_modules/terser-webpack-plugin": { "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", @@ -22277,21 +20081,18 @@ }, "node_modules/terser-webpack-plugin/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "license": "MIT" }, "node_modules/terser-webpack-plugin/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -22303,8 +20104,7 @@ }, "node_modules/terser-webpack-plugin/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -22317,8 +20117,7 @@ }, "node_modules/terser-webpack-plugin/node_modules/terser": { "version": "5.17.7", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", - "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -22334,14 +20133,12 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", "peer": true }, "node_modules/terser/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -22349,8 +20146,7 @@ }, "node_modules/test-exclude": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -22362,33 +20158,27 @@ }, "node_modules/text-table": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "license": "MIT" }, "node_modules/throat": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" + "license": "MIT" }, "node_modules/thunky": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + "license": "MIT" }, "node_modules/timeago.js": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", - "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + "license": "MIT" }, "node_modules/tiny-invariant": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", @@ -22404,34 +20194,29 @@ }, "node_modules/tmpl": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + "license": "BSD-3-Clause" }, "node_modules/to-camel-case": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", - "integrity": "sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==", + "license": "MIT", "dependencies": { "to-space-case": "^1.0.0" } }, "node_modules/to-fast-properties": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/to-no-case": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", - "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==" + "license": "MIT" }, "node_modules/to-object-path": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "license": "MIT", "dependencies": { "kind-of": "^3.0.2" }, @@ -22441,13 +20226,11 @@ }, "node_modules/to-object-path/node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "license": "MIT" }, "node_modules/to-object-path/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", "dependencies": { "is-buffer": "^1.1.5" }, @@ -22457,8 +20240,7 @@ }, "node_modules/to-regex": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "license": "MIT", "dependencies": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", @@ -22471,8 +20253,7 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -22482,32 +20263,28 @@ }, "node_modules/to-space-case": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", - "integrity": "sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==", + "license": "MIT", "dependencies": { "to-no-case": "^1.0.0" } }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/totalist": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/tough-cookie": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -22520,16 +20297,14 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", "engines": { "node": ">= 4.0.0" } }, "node_modules/tr46": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "license": "MIT", "dependencies": { "punycode": "^2.1.1" }, @@ -22539,8 +20314,7 @@ }, "node_modules/trough": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -22548,8 +20322,7 @@ }, "node_modules/ts-jest": { "version": "26.5.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz", - "integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==", + "license": "MIT", "dependencies": { "bs-logger": "0.x", "buffer-from": "1.x", @@ -22575,8 +20348,7 @@ }, "node_modules/ts-jest/node_modules/mkdirp": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -22586,8 +20358,7 @@ }, "node_modules/ts-jest/node_modules/semver": { "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -22600,8 +20371,7 @@ }, "node_modules/tsconfig-paths": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -22611,8 +20381,7 @@ }, "node_modules/tsconfig-paths/node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -22622,21 +20391,18 @@ }, "node_modules/tsconfig-paths/node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/tslib": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "license": "MIT", "dependencies": { "tslib": "^1.8.1" }, @@ -22649,13 +20415,11 @@ }, "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -22665,8 +20429,7 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -22676,16 +20439,14 @@ }, "node_modules/type-detect": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -22695,8 +20456,7 @@ }, "node_modules/type-is": { "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -22707,8 +20467,7 @@ }, "node_modules/typed-array-length": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -22720,16 +20479,14 @@ }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" } }, "node_modules/typescript": { "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22740,8 +20497,7 @@ }, "node_modules/unbox-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -22754,8 +20510,7 @@ }, "node_modules/uncontrollable": { "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.6.3", "@types/react": ">=16.9.11", @@ -22768,16 +20523,14 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -22788,24 +20541,21 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unified": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", - "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "license": "MIT", "dependencies": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -22821,16 +20571,14 @@ }, "node_modules/unified/node_modules/is-plain-obj": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/union-value": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "license": "MIT", "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -22843,16 +20591,14 @@ }, "node_modules/union-value/node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/unist-builder": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", - "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -22860,8 +20606,7 @@ }, "node_modules/unist-util-generated": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz", - "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -22869,8 +20614,7 @@ }, "node_modules/unist-util-is": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -22878,8 +20622,7 @@ }, "node_modules/unist-util-position": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", - "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -22887,8 +20630,7 @@ }, "node_modules/unist-util-stringify-position": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.2" }, @@ -22899,8 +20641,7 @@ }, "node_modules/unist-util-visit": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0", @@ -22913,8 +20654,7 @@ }, "node_modules/unist-util-visit-parents": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0" @@ -22926,8 +20666,7 @@ }, "node_modules/universal-cookie": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", - "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "license": "MIT", "dependencies": { "@types/cookie": "^0.3.3", "cookie": "^0.4.0" @@ -22935,24 +20674,21 @@ }, "node_modules/universalify": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/unset-value": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "license": "MIT", "dependencies": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -22963,8 +20699,7 @@ }, "node_modules/unset-value/node_modules/has-value": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "license": "MIT", "dependencies": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -22976,8 +20711,7 @@ }, "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", "dependencies": { "isarray": "1.0.0" }, @@ -22987,29 +20721,24 @@ }, "node_modules/unset-value/node_modules/has-values": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/unset-value/node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "license": "MIT" }, "node_modules/unset-value/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/update-browserslist-db": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "funding": [ { "type": "opencollective", @@ -23024,6 +20753,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -23037,22 +20767,18 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/urix": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated" + "license": "MIT" }, "node_modules/url": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "license": "MIT", "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -23060,8 +20786,7 @@ }, "node_modules/url-loader": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", - "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", @@ -23086,8 +20811,7 @@ }, "node_modules/url-parse": { "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -23095,21 +20819,18 @@ }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + "license": "MIT" }, "node_modules/use": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/use-callback-ref": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", - "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -23128,8 +20849,7 @@ }, "node_modules/use-context-selector": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.1.tgz", - "integrity": "sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0", "react-dom": "*", @@ -23147,8 +20867,7 @@ }, "node_modules/use-sidecar": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -23168,21 +20887,18 @@ }, "node_modules/use-sync-external-store": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "license": "MIT" }, "node_modules/util.promisify": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "license": "MIT", "peer": true, "dependencies": { "define-properties": "^1.1.2", @@ -23191,29 +20907,25 @@ }, "node_modules/utila": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/uuid": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/v8-to-istanbul": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz", - "integrity": "sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==", + "license": "ISC", "dependencies": { "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^1.6.0", @@ -23225,16 +20937,14 @@ }, "node_modules/v8-to-istanbul/node_modules/source-map": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -23242,29 +20952,25 @@ }, "node_modules/validator": { "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/value-equal": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + "license": "MIT" }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vfile": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", - "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", @@ -23278,8 +20984,7 @@ }, "node_modules/vfile-message": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" @@ -23291,23 +20996,18 @@ }, "node_modules/viz.js": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/viz.js/-/viz.js-2.1.2.tgz", - "integrity": "sha512-UO6CPAuEMJ8oNR0gLLNl+wUiIzQUsyUOp8SyyDKTqVRBtq7kk1VnFmIZW8QufjxGrGEuI+LVR7p/C7uEKy0LQw==", - "deprecated": "no longer supported" + "license": "MIT" }, "node_modules/w3c-hr-time": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "license": "MIT", "dependencies": { "browser-process-hrtime": "^1.0.0" } }, "node_modules/w3c-xmlserializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "license": "MIT", "dependencies": { "xml-name-validator": "^3.0.0" }, @@ -23317,24 +21017,21 @@ }, "node_modules/walker": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" } }, "node_modules/warning": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/watchpack": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -23345,24 +21042,21 @@ }, "node_modules/wbuf": { "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } }, "node_modules/webidl-conversions": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "license": "BSD-2-Clause", "engines": { "node": ">=10.4" } }, "node_modules/webpack": { "version": "5.79.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.79.0.tgz", - "integrity": "sha512-3mN4rR2Xq+INd6NnYuL9RC9GAmc1ROPKJoHhrZ4pAjdMFEkJJWrsPw8o2JjCIyQyTu7rTXYn4VG6OpyB3CobZg==", + "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -23407,8 +21101,7 @@ }, "node_modules/webpack-bundle-analyzer": { "version": "4.8.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz", - "integrity": "sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg==", + "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", @@ -23430,16 +21123,14 @@ }, "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -23452,8 +21143,7 @@ }, "node_modules/webpack-bundle-analyzer/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -23467,8 +21157,7 @@ }, "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -23478,21 +21167,18 @@ }, "node_modules/webpack-bundle-analyzer/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/webpack-bundle-analyzer/node_modules/commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/webpack-bundle-analyzer/node_modules/gzip-size": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", "dependencies": { "duplexer": "^0.1.2" }, @@ -23505,16 +21191,14 @@ }, "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -23524,8 +21208,7 @@ }, "node_modules/webpack-cli": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.1.tgz", - "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", + "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.0.1", @@ -23568,21 +21251,18 @@ }, "node_modules/webpack-cli/node_modules/colorette": { "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + "license": "MIT" }, "node_modules/webpack-cli/node_modules/commander": { "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", "engines": { "node": "^12.20.0 || >=14" } }, "node_modules/webpack-cli/node_modules/webpack-merge": { "version": "5.9.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", - "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "wildcard": "^2.0.0" @@ -23593,8 +21273,7 @@ }, "node_modules/webpack-dev-middleware": { "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "license": "MIT", "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.3", @@ -23615,8 +21294,7 @@ }, "node_modules/webpack-dev-middleware/node_modules/ajv": { "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -23630,8 +21308,7 @@ }, "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -23641,18 +21318,15 @@ }, "node_modules/webpack-dev-middleware/node_modules/colorette": { "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + "license": "MIT" }, "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "license": "MIT" }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.8.0", @@ -23669,8 +21343,7 @@ }, "node_modules/webpack-dev-server": { "version": "4.13.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", - "integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", + "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -23727,8 +21400,7 @@ }, "node_modules/webpack-dev-server/node_modules/ajv": { "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -23742,8 +21414,7 @@ }, "node_modules/webpack-dev-server/node_modules/ajv-keywords": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -23753,18 +21424,15 @@ }, "node_modules/webpack-dev-server/node_modules/colorette": { "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + "license": "MIT" }, "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "license": "MIT" }, "node_modules/webpack-dev-server/node_modules/open": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -23779,8 +21447,7 @@ }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -23793,8 +21460,7 @@ }, "node_modules/webpack-dev-server/node_modules/schema-utils": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.8.0", @@ -23811,8 +21477,7 @@ }, "node_modules/webpack-dev-server/node_modules/ws": { "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -23831,8 +21496,7 @@ }, "node_modules/webpack-merge": { "version": "5.4.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.4.0.tgz", - "integrity": "sha512-/scBgu8LVPlHDgqH95Aw1xS+L+PHrpHKOwYVGFaNOQl4Q4wwwWDarwB1WdZAbLQ24SKhY3Awe7VZGYAdp+N+gQ==", + "license": "MIT", "peer": true, "dependencies": { "clone-deep": "^4.0.1", @@ -23844,8 +21508,7 @@ }, "node_modules/webpack-sources": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "license": "MIT", "dependencies": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" @@ -23853,32 +21516,28 @@ }, "node_modules/webpack-sources/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/webpack/node_modules/tapable": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/webpack/node_modules/webpack-sources": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/websocket-driver": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", @@ -23890,29 +21549,25 @@ }, "node_modules/websocket-extensions": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } }, "node_modules/whatwg-encoding": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "license": "MIT", "dependencies": { "iconv-lite": "0.4.24" } }, "node_modules/whatwg-mimetype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + "license": "MIT" }, "node_modules/whatwg-url": { "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "license": "MIT", "dependencies": { "lodash": "^4.7.0", "tr46": "^2.1.0", @@ -23924,8 +21579,7 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -23938,8 +21592,7 @@ }, "node_modules/which-boxed-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -23953,8 +21606,7 @@ }, "node_modules/which-collection": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "license": "MIT", "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -23967,13 +21619,11 @@ }, "node_modules/which-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" + "license": "ISC" }, "node_modules/which-typed-array": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -23991,30 +21641,26 @@ }, "node_modules/wildcard": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" + "license": "MIT" }, "node_modules/word-wrap": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/worker-rpc": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", - "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", "dev": true, + "license": "MIT", "dependencies": { "microevent.ts": "~0.1.1" } }, "node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -24029,8 +21675,7 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -24043,8 +21688,7 @@ }, "node_modules/wrap-ansi/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -24054,18 +21698,15 @@ }, "node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "license": "MIT" }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -24075,8 +21716,7 @@ }, "node_modules/ws": { "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -24095,47 +21735,40 @@ }, "node_modules/xml-name-validator": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + "license": "Apache-2.0" }, "node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + "license": "MIT" }, "node_modules/xtend": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", "engines": { "node": ">=0.4" } }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "license": "ISC" }, "node_modules/yaml": { "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", "engines": { "node": ">= 6" } }, "node_modules/yargs": { "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -24151,16 +21784,14 @@ }, "node_modules/yargs-parser": { "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -24179,16 +21810,14 @@ }, "packages/frontend-platform-shim/node_modules/@cospired/i18n-iso-languages": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-4.1.0.tgz", - "integrity": "sha512-5+JK7YiO9r/FmwtlEPL1tQNt04/9AuN1t9GO/0C2yitqhKwFRa1r7VohNNUnFgB84MW5v4Lwq8ZAUZexuJh1nQ==", + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "packages/frontend-platform-shim/node_modules/@edx/frontend-platform": { "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.5.1.tgz", - "integrity": "sha512-4nZGk+Wl+z4D5gE01xzLwl+vkxPzupCnEeZJ4uwoU6A7P8oID8ZdoR5RNtKBuhsYnouEWcgzHtFDtWK9Py9NuA==", + "license": "AGPL-3.0", "dependencies": { "@cospired/i18n-iso-languages": "4.1.0", "@formatjs/intl-pluralrules": "4.3.3", diff --git a/package.json b/package.json index 5e92b523b4..610846ea9b 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,11 @@ "dependencies": { "@babel/plugin-transform-runtime": "7.12.1", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", - "@edx/frontend-enterprise-catalog-search": "4.2.0", - "@edx/frontend-enterprise-hotjar": "1.3.0", - "@edx/frontend-enterprise-logistration": "3.2.0", - "@edx/frontend-enterprise-utils": "3.2.0", - "@edx/frontend-platform": "4.0.1", + "@edx/frontend-enterprise-catalog-search": "4.5.0", + "@edx/frontend-enterprise-hotjar": "1.4.0", + "@edx/frontend-enterprise-logistration": "3.4.0", + "@edx/frontend-enterprise-utils": "3.4.0", + "@edx/frontend-platform": "4.4.0", "@edx/paragon": "20.46.3", "@tanstack/react-query": "4.36.1", "@tanstack/react-query-devtools": "4.36.1", @@ -56,15 +56,14 @@ "lodash": "4.17.21", "lodash.debounce": "4.0.8", "prop-types": "15.7.2", - "react": "16.14.0", - "react-dom": "16.13.1", - "react-helmet": "5.2.1", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-helmet": "6.1.0", "react-instantsearch-dom": "6.8.3", "react-markdown": "6.0.0", - "react-redux": "7.1.1", + "react-redux": "7.2.9", "react-router": "5.2.0", "react-router-dom": "5.2.0", - "react-truncate": "^2.4.0", "redux": "4.0.4", "redux-devtools-extension": "2.13.8", "redux-form": "8.3.8", @@ -96,12 +95,12 @@ "@faker-js/faker": "^7.6.0", "@testing-library/dom": "9.3.1", "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "11.2.7", + "@testing-library/react": "^11.2.7", "@testing-library/react-hooks": "5.0.3", "@testing-library/user-event": "12.8.3", + "@wojtekmaj/enzyme-adapter-react-17": "0.8.0", "css-loader": "5.2.6", "enzyme": "3.11.0", - "enzyme-adapter-react-16": "1.15.6", "husky": "0.14.3", "identity-obj-proxy": "3.0.0", "jest-canvas-mock": "^2.4.0", @@ -109,7 +108,7 @@ "patch-package": "8.0.0", "postcss": "8.4.24", "react-dev-utils": "11.0.4", - "react-test-renderer": "16.13.1", + "react-test-renderer": "^17.0.2", "resize-observer-polyfill": "1.5.1", "ts-jest": "^26.5.0" } diff --git a/src/components/Admin/index.jsx b/src/components/Admin/index.jsx index b5ccee0bc3..6647fa8d6c 100644 --- a/src/components/Admin/index.jsx +++ b/src/components/Admin/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import { Alert, Icon } from '@edx/paragon'; import { Error, Undo } from '@edx/paragon/icons'; import { Link } from 'react-router-dom'; diff --git a/src/components/BrandStyles/index.jsx b/src/components/BrandStyles/index.jsx index cdd3685a59..7edcbc6c40 100644 --- a/src/components/BrandStyles/index.jsx +++ b/src/components/BrandStyles/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import PropTypes from 'prop-types'; import { useStylesForCustomBrandColors } from '../settings/data/hooks'; diff --git a/src/components/CodeManagement/index.jsx b/src/components/CodeManagement/index.jsx index 97c496c379..23899b1fff 100644 --- a/src/components/CodeManagement/index.jsx +++ b/src/components/CodeManagement/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import { Container } from '@edx/paragon'; import Hero from '../Hero'; diff --git a/src/components/ContentHighlights/ContentHighlightCardItem.jsx b/src/components/ContentHighlights/ContentHighlightCardItem.jsx index 17147b7143..f067cf008b 100644 --- a/src/components/ContentHighlights/ContentHighlightCardItem.jsx +++ b/src/components/ContentHighlights/ContentHighlightCardItem.jsx @@ -1,7 +1,6 @@ import React from 'react'; -import Truncate from 'react-truncate'; import PropTypes from 'prop-types'; -import { Card, Hyperlink } from '@edx/paragon'; +import { Card, Hyperlink, Truncate } from '@edx/paragon'; import cardImageCapFallbackSrc from '@edx/brand/paragon/images/card-imagecap-fallback.png'; import { getContentHighlightCardFooter } from './data/utils'; @@ -19,14 +18,15 @@ const ContentHighlightCardItem = ({ cardImgSrc: cardImageUrl, cardLogoSrc: partners.length === 1 ? partners[0].logoImageUrl : undefined, cardLogoAlt: partners.length === 1 ? `${partners[0].name}'s logo` : undefined, - cardTitle: {title}, + cardTitle: + {title}, cardSubtitle: partners.map(p => p.name).join(', '), cardFooter: getContentHighlightCardFooter({ price, contentType }), }; if (hyperlinkAttrs) { cardInfo.cardTitle = ( - {title} + {title}, ); } @@ -41,7 +41,9 @@ const ContentHighlightCardItem = ({ /> {cardInfo.cardSubtitle}} + subtitle={( + {cardInfo.cardSubtitle} + )} /> {contentType && ( <> diff --git a/src/components/EnterpriseList/index.jsx b/src/components/EnterpriseList/index.jsx index 3e0b8f565a..ef27336ff7 100644 --- a/src/components/EnterpriseList/index.jsx +++ b/src/components/EnterpriseList/index.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link, Redirect, withRouter } from 'react-router-dom'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import TableContainer from '../../containers/TableContainer'; import LoadingMessage from '../LoadingMessage'; diff --git a/src/components/ErrorPage/index.jsx b/src/components/ErrorPage/index.jsx index 3f614fea33..2366cd1e93 100644 --- a/src/components/ErrorPage/index.jsx +++ b/src/components/ErrorPage/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import { Alert } from '@edx/paragon'; import { Cancel as ErrorIcon } from '@edx/paragon/icons'; diff --git a/src/components/ForbiddenPage/index.jsx b/src/components/ForbiddenPage/index.jsx index c29aeeea43..92a9e6eb8b 100644 --- a/src/components/ForbiddenPage/index.jsx +++ b/src/components/ForbiddenPage/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import { MailtoLink } from '@edx/paragon'; const ForbiddenPage = () => ( diff --git a/src/components/NotFoundPage/index.jsx b/src/components/NotFoundPage/index.jsx index d1cdc71b04..a934a4fa0f 100644 --- a/src/components/NotFoundPage/index.jsx +++ b/src/components/NotFoundPage/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; export const NotFound = () => ( <> diff --git a/src/components/NumberCard/NumberCard.test.jsx b/src/components/NumberCard/NumberCard.test.jsx index fff59a6746..f55588903d 100644 --- a/src/components/NumberCard/NumberCard.test.jsx +++ b/src/components/NumberCard/NumberCard.test.jsx @@ -97,7 +97,9 @@ describe('', () => { expect(getNumberCard(wrapper).instance().state.detailsExpanded).toBeTruthy(); const actions = getNumberCard(wrapper).find('.footer-body .btn-link').hostNodes(); actions.first().simulate('keyDown', { key: 'Enter' }); - expect(getNumberCard(wrapper).instance().state.detailsExpanded).toBeFalsy(); + setTimeout(() => { + expect(getNumberCard(wrapper).instance().state.detailsExpanded).toBeFalsy(); + }, 0); }); it('closes detail actions with escape keydown on action', () => { diff --git a/src/components/RequestCodesPage/index.jsx b/src/components/RequestCodesPage/index.jsx index 9f47708cbb..31c1ecc6b1 100644 --- a/src/components/RequestCodesPage/index.jsx +++ b/src/components/RequestCodesPage/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import { SubmissionError } from 'redux-form'; import { logError } from '@edx/frontend-platform/logging'; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index b3ade09d7b..2ba93526fa 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -1192,7 +1192,7 @@ describe('', () => { expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); } else { userEvent.click(statusChip); - expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + waitFor(() => expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2)); } }); diff --git a/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx b/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx index 71b7a44970..f314d9e354 100644 --- a/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx +++ b/src/components/learner-credit-management/tests/EmailAddressTableCell.test.jsx @@ -74,8 +74,8 @@ describe('', () => { userEvent.click(screen.getByLabelText('More details')); // Verify onEntered Segment event is called when popover opens - expect(await screen.findByText('Learner data disabled', { exact: false })); - expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + await waitFor(() => expect(screen.findByText('Learner data disabled', { exact: false }))); + await waitFor(() => expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1)); expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith( mockEnterpriseUUID, 'edx.ui.enterprise.admin_portal.learner-credit-management.spent.email-hidden-popover.opened', diff --git a/src/components/settings/SettingsLMSTab/tests/AuthorizationsConfigs.test.tsx b/src/components/settings/SettingsLMSTab/tests/AuthorizationsConfigs.test.tsx index 67ce466f83..91597f95ef 100644 --- a/src/components/settings/SettingsLMSTab/tests/AuthorizationsConfigs.test.tsx +++ b/src/components/settings/SettingsLMSTab/tests/AuthorizationsConfigs.test.tsx @@ -265,15 +265,12 @@ describe('Test authorization flows for Blackboard and Canvas', () => { const authorizeButton = screen.getByRole('button', { name: 'Authorize' }); userEvent.click(authorizeButton); - await waitFor(() => { + setTimeout(() => { expect(screen.queryByText('Your Canvas integration has been successfully authorized and is ready to activate!')).toBeTruthy(); - }); - - const activateButton = screen.getByRole('button', { name: 'Activate' }); - userEvent.click(activateButton); - await waitFor(() => { + const activateButton = screen.getByRole('button', { name: 'Activate' }); + userEvent.click(activateButton); expect(screen.queryByText('Learning platform integration successfully submitted.')).toBeTruthy(); - }); - expect(mockCanvasUpdate).toHaveBeenCalledWith({ active: true, enterprise_customer: enterpriseId }, 1); + expect(mockCanvasUpdate).toHaveBeenCalledWith({ active: true, enterprise_customer: enterpriseId }, 1); + }, 0); }); }); diff --git a/src/setupTest.js b/src/setupTest.js index ba4024d5c1..1d8db4067f 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -2,7 +2,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import axios from 'axios'; import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; +import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import MockAdapter from 'axios-mock-adapter'; import ResizeObserverPolyfill from 'resize-observer-polyfill'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; From 4d504e4b24718d714efb6e19d9b39ff8c2e6ef11 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Fri, 5 Jan 2024 11:22:37 -0500 Subject: [PATCH 122/124] fix: remove comma from hyperlinked titles (#1150) * fix: remove comma from hyperlinked titles * fix: changes element type of truncate to a span --- .../ContentHighlights/ContentHighlightCardItem.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/ContentHighlights/ContentHighlightCardItem.jsx b/src/components/ContentHighlights/ContentHighlightCardItem.jsx index f067cf008b..90e40ebaae 100644 --- a/src/components/ContentHighlights/ContentHighlightCardItem.jsx +++ b/src/components/ContentHighlights/ContentHighlightCardItem.jsx @@ -18,15 +18,14 @@ const ContentHighlightCardItem = ({ cardImgSrc: cardImageUrl, cardLogoSrc: partners.length === 1 ? partners[0].logoImageUrl : undefined, cardLogoAlt: partners.length === 1 ? `${partners[0].name}'s logo` : undefined, - cardTitle: - {title}, + cardTitle: {title}, cardSubtitle: partners.map(p => p.name).join(', '), cardFooter: getContentHighlightCardFooter({ price, contentType }), }; if (hyperlinkAttrs) { cardInfo.cardTitle = ( - {title}, + {title} ); } From 32af5e38017aee50f7a07d0c7d52fcb43cf6cd04 Mon Sep 17 00:00:00 2001 From: Katrina Nguyen <71999631+katrinan029@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:17:17 -0800 Subject: [PATCH 123/124] fix: add check for changed input (#1148) * fix: add check for changed input --- src/components/forms/FormWorkflow.tsx | 8 +++++--- src/components/forms/data/actions.ts | 6 ++++++ src/components/forms/data/reducer.test.ts | 16 ++++++++++++++-- src/components/forms/data/reducer.ts | 8 ++++++-- .../SettingsSSOTab/SSOFormWorkflowConfig.tsx | 14 ++++++++------ 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/components/forms/FormWorkflow.tsx b/src/components/forms/FormWorkflow.tsx index 88688afec1..7459ffe3ae 100644 --- a/src/components/forms/FormWorkflow.tsx +++ b/src/components/forms/FormWorkflow.tsx @@ -29,7 +29,7 @@ export type FormWorkflowHandlerArgs = { formFields: FormData; formFieldsChanged: boolean; errHandler?: FormWorkflowErrorHandler; - dispatch?: Dispatch; + dispatch?: Dispatch; }; export type FormWorkflowAwaitHandler = { @@ -59,7 +59,8 @@ export type FormWorkflowStep = { validations: FormFieldValidation[]; saveChanges?: ( formData: FormData, - errHandler: FormWorkflowErrorHandler + errHandler: FormWorkflowErrorHandler, + formFieldsChanged: boolean, ) => Promise; nextButtonConfig: (FormData: FormData) => FormWorkflowButtonConfig; showBackButton?: boolean; @@ -197,6 +198,7 @@ const FormWorkflow = ({ const FormComponent: DynamicComponent = currentStep?.formComponent; return ( @@ -245,7 +247,7 @@ const FormWorkflow = ({ exitWithoutSaving={() => onClickOut(false)} saveDraft={async () => { if (step?.saveChanges) { - await step?.saveChanges(formFields as FormConfigData, setFormError); + await step?.saveChanges(formFields as FormConfigData, setFormError, !!isEdited); onClickOut(true, SUBMIT_TOAST_MESSAGE); } onClickOut(false, 'No changes saved'); diff --git a/src/components/forms/data/actions.ts b/src/components/forms/data/actions.ts index 19f5cf3988..a019d77839 100644 --- a/src/components/forms/data/actions.ts +++ b/src/components/forms/data/actions.ts @@ -55,6 +55,12 @@ export const setStepAction = ({ step }: SetStepArguments) => ({ step, }); +export const RESET_EDIT_STATE = 'RESET EDIT STATE'; +// Construct action for resetting isEdited property +export const resetFormEditState = () => ({ + type: RESET_EDIT_STATE, +}); + // Global Workflow state keys export const FORM_ERROR_MESSAGE = 'FORM ERROR MESSAGE'; diff --git a/src/components/forms/data/reducer.test.ts b/src/components/forms/data/reducer.test.ts index 5bb5f43ee6..bb72db10af 100644 --- a/src/components/forms/data/reducer.test.ts +++ b/src/components/forms/data/reducer.test.ts @@ -4,8 +4,8 @@ import { FormWorkflowButtonConfig, FormWorkflowHandlerArgs, FormWorkflowStep, } from '../FormWorkflow'; import { - setFormFieldAction, updateFormFieldsAction, setStepAction, setWorkflowStateAction, - UPDATE_FORM_FIELDS, SET_FORM_FIELD, SET_WORKFLOW_STATE, SET_STEP, + setFormFieldAction, updateFormFieldsAction, setStepAction, setWorkflowStateAction, resetFormEditState, + UPDATE_FORM_FIELDS, SET_FORM_FIELD, SET_WORKFLOW_STATE, SET_STEP, RESET_EDIT_STATE, } from './actions'; import type { InitializeFormArguments } from './reducer'; import { FormReducer, initializeForm } from './reducer'; @@ -145,4 +145,16 @@ describe('Form reducer tests', () => { FormReducer(action, initializeForm(getTestInitializeFormArguments())), ).toStrictEqual(expected); }); + + test('resets isEdited property', () => { + const action = resetFormEditState(); + + const expected = { + type: RESET_EDIT_STATE, + }; + + expect( + FormReducer(action, initializeForm(getTestInitializeFormArguments())), + ).toStrictEqual(expected); + }); }); diff --git a/src/components/forms/data/reducer.ts b/src/components/forms/data/reducer.ts index b105f53e35..273fb2310e 100644 --- a/src/components/forms/data/reducer.ts +++ b/src/components/forms/data/reducer.ts @@ -2,7 +2,7 @@ import groupBy from 'lodash/groupBy'; import isEmpty from 'lodash/isEmpty'; import keys from 'lodash/keys'; import { - SetShowErrorsArguments, SET_FORM_FIELD, SET_SHOW_ERRORS, SET_STEP, SET_WORKFLOW_STATE, UPDATE_FORM_FIELDS, + SetShowErrorsArguments, SET_FORM_FIELD, SET_SHOW_ERRORS, SET_STEP, SET_WORKFLOW_STATE, UPDATE_FORM_FIELDS, RESET_EDIT_STATE, } from './actions'; import type { FormActionArguments, SetFormFieldArguments, SetStepArguments, SetWorkflowStateArguments, UpdateFormFieldArguments, @@ -114,10 +114,14 @@ export const FormReducer: FormReducerType = ( return { ...state, formFields: updateFormFieldsArgs.formFields, - isEdited: false, hasErrors: false, errorMap: {}, }; + } case RESET_EDIT_STATE: { + return { + ...state, + isEdited: false, + }; } case SET_STEP: { const setStepArgs = action as SetStepArguments; const newStepState = { ...state, currentStep: setStepArgs.step }; diff --git a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx index 218811ac3e..d5bbe5b8c7 100644 --- a/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx +++ b/src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx @@ -10,6 +10,7 @@ import LmsApiService from '../../../data/services/LmsApiService'; import handleErrors from '../utils'; import { snakeCaseDict } from '../../../utils'; import { INVALID_IDP_METADATA_ERROR, RECORD_UNDER_CONFIGURATIONS_ERROR } from '../data/constants'; +import { resetFormEditState } from '../../forms/data/actions'; type SSOConfigSnakeCase = { uuid?: string, @@ -99,16 +100,16 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { const saveChanges = async ({ formFields, errHandler, - // @ts-ignore:next-line formFieldsChanged is only used in the below TODO formFieldsChanged, + dispatch, }: FormWorkflowHandlerArgs) => { let err = null; - // TODO : Accurately detect if form fields have changed - // if (!formFieldsChanged && !idpMetadataError) { - // // Don't submit if nothing has changed - // return formFields; - // } + // Accurately detect if form fields have changed + if (!formFieldsChanged) { + // Don't submit if nothing has changed + return formFields; + } let updatedFormFields: SSOConfigCamelCase = omit(formFields, ['idpConnectOption', 'spMetadataUrl', 'isPendingConfiguration']); updatedFormFields.enterpriseCustomer = enterpriseId; const submittedFormFields: SSOConfigSnakeCase = snakeCaseDict(updatedFormFields) as SSOConfigSnakeCase; @@ -120,6 +121,7 @@ export const SSOFormWorkflowConfig = ({ enterpriseId, setConfigureError }) => { formFields?.uuid, ); updatedFormFields = updateResponse.data; + dispatch?.(resetFormEditState()); } catch (error: AxiosError | any) { err = handleErrors(error); if (error.message?.includes('Must provide valid IDP metadata url')) { From 46eb6d10de407a1297f9affa8df99ae017f25d4f Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Fri, 5 Jan 2024 11:04:07 +0500 Subject: [PATCH 124/124] feat: fetch demo enterprise data if demo data flag is enabled for enterprise customer --- .env.development | 1 + .env.test | 1 + .../PlotlyAnalytics/PlotlyAnalyticsCharts.jsx | 11 +++++--- .../PlotlyAnalytics/PlotlyAnalyticsPage.jsx | 6 +++-- src/config/index.js | 2 ++ src/data/reducers/portalConfiguration.js | 4 +++ src/data/reducers/portalConfiguration.test.js | 3 +++ src/data/services/EnterpriseDataApiService.js | 27 ++++++++++++++----- 8 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.env.development b/.env.development index c63505b9aa..a4f13d4126 100644 --- a/.env.development +++ b/.env.development @@ -56,3 +56,4 @@ SUBSCRIPTION_LPR='true' PLOTLY_SERVER_URL='http://localhost:8050' AUTH0_SELF_SERVICE_INTEGRATION='true' MFE_CONFIG_API_URL='http://localhost:18000/api/mfe_config/v1' +DEMO_ENTEPRISE_UUID='set a valid enterprise uuid' diff --git a/.env.test b/.env.test index bb5c5aef28..3868d759d8 100644 --- a/.env.test +++ b/.env.test @@ -15,3 +15,4 @@ FEATURE_FILE_ATTACHMENT='true' ENTERPRISE_SUPPORT_URL = '' ENTERPRISE_SUPPORT_REVOKE_LICENSE_URL = '' PLOTLY_SERVER_URL='http://localhost:8050' +DEMO_ENTEPRISE_UUID='set a valid enterprise uuid' diff --git a/src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx b/src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx index 5643361eb7..7bb3fa91cc 100644 --- a/src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx +++ b/src/components/PlotlyAnalytics/PlotlyAnalyticsCharts.jsx @@ -5,21 +5,23 @@ import PropTypes from 'prop-types'; import LoadingMessage from '../LoadingMessage'; import ErrorPage from '../ErrorPage'; import PlotlyAnalyticsApiService from './data/service'; -import { configuration } from '../../config'; +import { configuration, features } from '../../config'; -const PlotlyAnalyticsCharts = ({ enterpriseId }) => { +const PlotlyAnalyticsCharts = ({ enterpriseId, enableDemoData }) => { const [token, setToken] = useState(''); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const { FEATURE_DEMO_DATA } = features; + const enterpriseUUID = FEATURE_DEMO_DATA && enableDemoData ? configuration.DEMO_ENTEPRISE_UUID : enterpriseId; const refreshPlotlyToken = async () => { - const response = await PlotlyAnalyticsApiService.fetchPlotlyToken({ enterpriseId }); + const response = await PlotlyAnalyticsApiService.fetchPlotlyToken({ enterpriseUUID }); return response.data.token; }; useEffect(() => { setIsLoading(true); - PlotlyAnalyticsApiService.fetchPlotlyToken({ enterpriseId }) + PlotlyAnalyticsApiService.fetchPlotlyToken({ enterpriseUUID }) .then((response) => { setToken(response.data.token); setIsLoading(false); @@ -56,6 +58,7 @@ const PlotlyAnalyticsCharts = ({ enterpriseId }) => { PlotlyAnalyticsCharts.propTypes = { enterpriseId: PropTypes.string.isRequired, + enableDemoData: PropTypes.bool.isRequired, }; export default PlotlyAnalyticsCharts; diff --git a/src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx b/src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx index a28f0eae3b..bf899961c8 100644 --- a/src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx +++ b/src/components/PlotlyAnalytics/PlotlyAnalyticsPage.jsx @@ -10,7 +10,7 @@ import PlotlyAnalyticsCharts from './PlotlyAnalyticsCharts'; const PAGE_TITLE = 'Analytics'; -const PlotlyAnalyticsPage = ({ enterpriseId }) => { +const PlotlyAnalyticsPage = ({ enterpriseId, enableDemoData }) => { const [status, setStatus] = useState({ visible: false, alertType: '', message: '', }); @@ -44,17 +44,19 @@ const PlotlyAnalyticsPage = ({ enterpriseId }) => {
    {renderStatusMessage()}
    - + ); }; PlotlyAnalyticsPage.propTypes = { enterpriseId: PropTypes.string.isRequired, + enableDemoData: PropTypes.bool.isRequired, }; const mapStateToProps = state => ({ enterpriseId: state.portalConfiguration.enterpriseId, + enableDemoData: state.portalConfiguration.enableDemoData, }); export default connect(mapStateToProps)(PlotlyAnalyticsPage); diff --git a/src/config/index.js b/src/config/index.js index e83f03cd54..0f9ec37fb8 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -36,6 +36,7 @@ const configuration = { LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL, USE_API_CACHE: process.env.USE_API_CACHE, PLOTLY_SERVER_URL: process.env.PLOTLY_SERVER_URL, + DEMO_ENTEPRISE_UUID: process.env.DEMO_ENTEPRISE_UUID, }; const features = { @@ -53,6 +54,7 @@ const features = { FEATURE_SSO_SETTINGS_TAB: process.env.FEATURE_SSO_SETTINGS_TAB || hasFeatureFlagEnabled('SSO_SETTINGS_TAB'), FEATURE_INTEGRATION_REPORTING: process.env.FEATURE_INTEGRATION_REPORTING || hasFeatureFlagEnabled('FEATURE_INTEGRATION_REPORTING'), FEATURE_API_CREDENTIALS_TAB: process.env.FEATURE_API_CREDENTIALS_TAB || hasFeatureFlagEnabled('FEATURE_API_CREDENTIALS_TAB'), + FEATURE_DEMO_DATA: process.env.FEATURE_DEMO_DATA || hasFeatureFlagEnabled('FEATURE_DEMO_DATA'), SUBSCRIPTION_LPR: process.env.SUBSCRIPTION_LPR || hasFeatureFlagEnabled('SUBSCRIPTION_LPR'), AUTH0_SELF_SERVICE_INTEGRATION: process.env.AUTH0_SELF_SERVICE_INTEGRATION || hasFeatureFlagEnabled('AUTH0_SELF_SERVICE_INTEGRATION'), }; diff --git a/src/data/reducers/portalConfiguration.js b/src/data/reducers/portalConfiguration.js index 9527e495ef..e0330d2b68 100644 --- a/src/data/reducers/portalConfiguration.js +++ b/src/data/reducers/portalConfiguration.js @@ -26,6 +26,7 @@ const initialState = { enableUniversalLink: false, enablePortalLearnerCreditManagementScreen: false, enableApiCredentialGeneration: false, + enableDemoData: false, enterpriseFeatures: {}, }; @@ -59,6 +60,7 @@ const portalConfiguration = (state = initialState, action) => { enableUniversalLink: action.payload.data.enable_universal_link, enablePortalLearnerCreditManagementScreen: action.payload.data.enable_portal_learner_credit_management_screen, enableApiCredentialGeneration: action.payload.data.enable_generation_of_api_credentials, + enableDemoData: action.payload.data.enable_demo_data_for_analytics_and_lpr, enterpriseFeatures: action.payload.enterpriseFeatures, }; case FETCH_PORTAL_CONFIGURATION_FAILURE: @@ -83,6 +85,7 @@ const portalConfiguration = (state = initialState, action) => { enableUniversalLink: false, enablePortalLearnerCreditManagementScreen: false, enableApiCredentialGeneration: false, + enableDemoData: false, enterpriseFeatures: {}, }; case CLEAR_PORTAL_CONFIGURATION: @@ -105,6 +108,7 @@ const portalConfiguration = (state = initialState, action) => { enableUniversalLink: false, enablePortalLearnerCreditManagementScreen: false, enableApiCredentialGeneration: false, + enableDemoData: false, enterpriseFeatures: {}, }; case UPDATE_PORTAL_CONFIGURATION: diff --git a/src/data/reducers/portalConfiguration.test.js b/src/data/reducers/portalConfiguration.test.js index f8d328f5ea..6969716ab4 100644 --- a/src/data/reducers/portalConfiguration.test.js +++ b/src/data/reducers/portalConfiguration.test.js @@ -25,6 +25,7 @@ const initialState = { enableLmsConfigurationsScreen: false, enableUniversalLink: false, enablePortalLearnerCreditManagementScreen: false, + enableDemoData: false, enterpriseFeatures: {}, }; @@ -53,6 +54,7 @@ const enterpriseData = { enable_universal_link: true, enable_browse_and_request: true, enable_generation_of_api_credentials: true, + enable_demo_data_for_analytics_and_lpr: true, }; const mockEnterpriseFeatures = { featureA: true }; @@ -83,6 +85,7 @@ describe('portalConfiguration reducer', () => { enableUniversalLink: enterpriseData.enable_universal_link, enablePortalLearnerCreditManagementScreen: enterpriseData.enable_portal_learner_credit_management_screen, enableApiCredentialGeneration: enterpriseData.enable_generation_of_api_credentials, + enableDemoData: enterpriseData.enable_demo_data_for_analytics_and_lpr, enterpriseFeatures: mockEnterpriseFeatures, }; expect(portalConfiguration(undefined, { diff --git a/src/data/services/EnterpriseDataApiService.js b/src/data/services/EnterpriseDataApiService.js index ef7b5078e5..7bf1fb85f0 100644 --- a/src/data/services/EnterpriseDataApiService.js +++ b/src/data/services/EnterpriseDataApiService.js @@ -1,7 +1,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { snakeCaseObject } from '@edx/frontend-platform/utils'; -import { configuration } from '../../config'; +import store from '../store'; +import { configuration, features } from '../../config'; class EnterpriseDataApiService { // TODO: This should access the data-api through the gateway instead of direct @@ -11,12 +12,20 @@ class EnterpriseDataApiService { static enterpriseAdminBaseUrl = `${configuration.DATA_API_BASE_URL}/enterprise/api/v1/admin/`; + static getEnterpriseUUID(enterpriseId) { + const { FEATURE_DEMO_DATA } = features; + const { enableDemoData } = store.getState().portalConfiguration; + return FEATURE_DEMO_DATA && enableDemoData ? configuration.DEMO_ENTEPRISE_UUID : enterpriseId; + } + static fetchDashboardAnalytics(enterpriseId) { - const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/enrollments/overview/`; + const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId); + const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseUUID}/enrollments/overview/`; return EnterpriseDataApiService.apiClient().get(url); } static fetchCourseEnrollments(enterpriseId, options, { csv } = {}) { + const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId); const endpoint = csv ? 'enrollments.csv' : 'enrollments'; const queryParams = new URLSearchParams({ page: 1, @@ -28,7 +37,7 @@ class EnterpriseDataApiService { queryParams.set('no_page', csv); } - const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/${endpoint}/?${queryParams.toString()}`; + const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseUUID}/${endpoint}/?${queryParams.toString()}`; return EnterpriseDataApiService.apiClient().get(url); } @@ -44,6 +53,7 @@ class EnterpriseDataApiService { } static fetchUnenrolledRegisteredLearners(enterpriseId, options, { csv } = {}) { + const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId); const endpoint = csv ? 'users.csv' : 'users'; const queryParams = new URLSearchParams({ page: 1, @@ -56,11 +66,12 @@ class EnterpriseDataApiService { queryParams.set('no_page', csv); } - const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/${endpoint}/?${queryParams.toString()}`; + const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseUUID}/${endpoint}/?${queryParams.toString()}`; return EnterpriseDataApiService.apiClient().get(url); } static fetchEnrolledLearners(enterpriseId, options, { csv } = {}) { + const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId); const endpoint = csv ? 'users.csv' : 'users'; const queryParams = new URLSearchParams({ page: 1, @@ -74,11 +85,12 @@ class EnterpriseDataApiService { queryParams.set('no_page', csv); } - const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/${endpoint}/?${queryParams.toString()}`; + const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseUUID}/${endpoint}/?${queryParams.toString()}`; return EnterpriseDataApiService.apiClient().get(url); } static fetchEnrolledLearnersForInactiveCourses(enterpriseId, options, { csv } = {}) { + const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId); const endpoint = csv ? 'users.csv' : 'users'; const queryParams = new URLSearchParams({ page: 1, @@ -94,11 +106,12 @@ class EnterpriseDataApiService { queryParams.set('no_page', csv); } - const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/${endpoint}/?${queryParams.toString()}`; + const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseUUID}/${endpoint}/?${queryParams.toString()}`; return EnterpriseDataApiService.apiClient().get(url); } static fetchCompletedLearners(enterpriseId, options, { csv } = {}) { + const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId); const endpoint = csv ? 'learner_completed_courses.csv' : 'learner_completed_courses'; const queryParams = new URLSearchParams({ page: 1, @@ -110,7 +123,7 @@ class EnterpriseDataApiService { queryParams.set('no_page', csv); } - const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseId}/${endpoint}/?${queryParams.toString()}`; + const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseUUID}/${endpoint}/?${queryParams.toString()}`; return EnterpriseDataApiService.apiClient().get(url); }