Skip to content

Commit

Permalink
Merge pull request #366 from openedx/knguyen2/ent-7958
Browse files Browse the repository at this point in the history
feat: adds budget distribution mode selection component in provisioning tool
  • Loading branch information
katrinan029 authored Dec 1, 2023
2 parents 895ac85 + 6bc906a commit 6edeec1
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import ProvisioningFormPerLearnerCap from './ProvisioningFormPerLearnerCap';
import ProvisioningFormPerLearnerCapAmount from './ProvisioningFormPerLearnerCapAmount';
import PROVISIONING_PAGE_TEXT from '../../data/constants';
import { indexOnlyPropType, selectProvisioningContext } from '../../data/utils';

const ProvisioningFormPerLearnerCapContainer = ({ index }) => {
const [formData] = selectProvisioningContext('formData');
const { POLICY_TYPE: { OPTIONS } } = PROVISIONING_PAGE_TEXT.FORM;
return (
<>
<ProvisioningFormPerLearnerCap index={index} />
{formData.policies[index]?.policyType === OPTIONS.LEARNER_SELECTS.VALUE
&& <ProvisioningFormPerLearnerCap index={index} />}
{formData.policies[index]?.perLearnerCap && <ProvisioningFormPerLearnerCapAmount index={index} />}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import ProvisioningFormAccountDetails from './ProvisioningFormAccountDetails';
import ProvisioningFormPerLearnerCapContainer from './ProvisioningFormPerLearnerCapContainer';
import { indexOnlyPropType } from '../../data/utils';
import ProvisioningFormDescription from './ProvisioningFormDescription';
import ProvisioningFormPolicyType from './ProvisioningFormPolicyType';

const ProvisioningFormPolicyContainer = ({ title, index }) => (
<div className="mt-5">
<h2>{title}</h2>
<ProvisioningFormAccountDetails index={index} />
<ProvisioningFormDescription index={index} />
<ProvisioningFormCatalogContainer index={index} />
<ProvisioningFormPolicyType index={index} />
<ProvisioningFormPerLearnerCapContainer index={index} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Form,
} from '@edx/paragon';
import { v4 as uuidv4 } from 'uuid';
import React, { useState } from 'react';
import PROVISIONING_PAGE_TEXT from '../../data/constants';
import useProvisioningContext from '../../data/hooks';
import { indexOnlyPropType, selectProvisioningContext } from '../../data/utils';

const ProvisioningFormPolicyType = ({ index }) => {
const { perLearnerCap, setPolicyType, setInvalidPolicyFields } = useProvisioningContext();
const { POLICY_TYPE } = PROVISIONING_PAGE_TEXT.FORM;
const [formData, showInvalidField] = selectProvisioningContext('formData', 'showInvalidField');
const { policies } = showInvalidField;
const isPolicyTypeDefinedAndFalse = policies[index]?.policyType === false;

const [value, setValue] = useState(null);

const handleChange = (e) => {
const policyTypeValue = e.target.value;
if (policyTypeValue === POLICY_TYPE.OPTIONS.LEARNER_SELECTS.DESCRIPTION) {
setPolicyType({
policyType: POLICY_TYPE.OPTIONS.LEARNER_SELECTS.VALUE,
accessMethod: POLICY_TYPE.OPTIONS.LEARNER_SELECTS.ACCESS_METHOD,
}, index);
} else if (policyTypeValue === POLICY_TYPE.OPTIONS.ADMIN_SELECTS.DESCRIPTION) {
setPolicyType({
policyType: POLICY_TYPE.OPTIONS.ADMIN_SELECTS.VALUE,
accessMethod: POLICY_TYPE.OPTIONS.ADMIN_SELECTS.ACCESS_METHOD,
}, index);
perLearnerCap({
perLearnerCap: false,
}, index);
setInvalidPolicyFields({ perLearnerCap: true }, index);
}
setValue(policyTypeValue);
setInvalidPolicyFields({ policyType: true }, index);
};

return (
<article className="mt-4.5">
<div>
<h3>{POLICY_TYPE.TITLE}</h3>
</div>
<Form.Group
className="mt-3.5"
>
<Form.Label className="mb-2.5">{POLICY_TYPE.LABEL}</Form.Label>
<Form.RadioSet
name={`display-policy-type-${index}`}
onChange={handleChange}
value={value || formData.policies[index]?.policyType}
>
{
Object.values(POLICY_TYPE.OPTIONS).map(({ DESCRIPTION }) => (
<Form.Radio
value={DESCRIPTION}
type="radio"
key={uuidv4()}
data-testid={DESCRIPTION}
isInvalid={isPolicyTypeDefinedAndFalse}
>
{DESCRIPTION}
</Form.Radio>
))
}
</Form.RadioSet>
{isPolicyTypeDefinedAndFalse && (
<Form.Control.Feedback
type="invalid"
>
{POLICY_TYPE.ERROR}
</Form.Control.Feedback>
)}
</Form.Group>
</article>
);
};

ProvisioningFormPolicyType.propTypes = indexOnlyPropType;

export default ProvisioningFormPolicyType;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ProvisioningContext, initialStateValue } from '../../../../testData/Pro
import PROVISIONING_PAGE_TEXT, { INITIAL_CATALOG_QUERIES } from '../../../data/constants';
import ProvisioningFormPerLearnerCapContainer from '../ProvisioningFormPerLearnerCapContainer';

const { LEARNER_CAP_DETAIL, LEARNER_CAP } = PROVISIONING_PAGE_TEXT.FORM;
const { LEARNER_CAP_DETAIL, LEARNER_CAP, POLICY_TYPE } = PROVISIONING_PAGE_TEXT.FORM;

const ProvisioningFormPerLearnerCapContainerWrapper = ({
value = initialStateValue,
Expand All @@ -16,19 +16,25 @@ const ProvisioningFormPerLearnerCapContainerWrapper = ({
</ProvisioningContext>
);

const updatedInitialState = {
...initialStateValue,
multipleFunds: false,
formData: {
...initialStateValue.formData,
policies: [
{
...INITIAL_CATALOG_QUERIES.defaultQuery,
policyType: POLICY_TYPE.OPTIONS.LEARNER_SELECTS.VALUE,
},
],
},
};

describe('PerLearnerCapContainer', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('renders single policy state', () => {
const updatedInitialState = {
...initialStateValue,
multipleFunds: false,
formData: {
...initialStateValue.formData,
policies: INITIAL_CATALOG_QUERIES.defaultQuery,
},
};
renderWithRouter(
<ProvisioningFormPerLearnerCapContainerWrapper
value={updatedInitialState}
Expand All @@ -40,14 +46,6 @@ describe('PerLearnerCapContainer', () => {
expect(screen.getByText(LEARNER_CAP.SUB_TITLE)).toBeTruthy();
});
it('updates the state of form on change, true', () => {
const updatedInitialState = {
...initialStateValue,
multipleFunds: false,
formData: {
...initialStateValue.formData,
policies: INITIAL_CATALOG_QUERIES.defaultQuery,
},
};
renderWithRouter(
<ProvisioningFormPerLearnerCapContainerWrapper
value={updatedInitialState}
Expand All @@ -60,23 +58,38 @@ describe('PerLearnerCapContainer', () => {
expect(screen.getByText(LEARNER_CAP_DETAIL.TITLE)).toBeTruthy();
});
it('updates the state of form on change, false', () => {
const updatedInitialState = {
renderWithRouter(
<ProvisioningFormPerLearnerCapContainerWrapper
value={updatedInitialState}
index={0}
/>,
);

const input = screen.getByTestId(LEARNER_CAP.OPTIONS.no);
fireEvent.click(input);
expect(screen.queryByText(LEARNER_CAP_DETAIL.TITLE)).not.toBeTruthy();
});
it('does not render learner cap if policy type is AssignedLearnerCreditAccessPolicy', () => {
const updatedState = {
...initialStateValue,
multipleFunds: false,
formData: {
...initialStateValue.formData,
policies: INITIAL_CATALOG_QUERIES.defaultQuery,
policies: [
{
...INITIAL_CATALOG_QUERIES.defaultQuery,
policyType: POLICY_TYPE.OPTIONS.ADMIN_SELECTS.VALUE,
},
],
},
};

renderWithRouter(
<ProvisioningFormPerLearnerCapContainerWrapper
value={updatedInitialState}
value={updatedState}
index={0}
/>,
);

const input = screen.getByTestId(LEARNER_CAP.OPTIONS.no);
fireEvent.click(input);
expect(screen.queryByText(LEARNER_CAP_DETAIL.TITLE)).not.toBeTruthy();
expect(screen.queryByText(LEARNER_CAP.TITLE)).not.toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-disable react/prop-types */
import { renderWithRouter } from '@edx/frontend-enterprise-utils';
import { screen } from '@testing-library/react';
import { screen, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProvisioningFormPolicyContainer from '../ProvisioningFormPolicyContainer';
import { ProvisioningContext, initialStateValue } from '../../../../testData/Provisioning';
import PROVISIONING_PAGE_TEXT, { INITIAL_CATALOG_QUERIES } from '../../../data/constants';

const { ACCOUNT_DETAIL } = PROVISIONING_PAGE_TEXT.FORM;
const { ACCOUNT_DETAIL, POLICY_TYPE } = PROVISIONING_PAGE_TEXT.FORM;

const ProvisioningFormPolicyContainerWrapper = ({
value = initialStateValue,
Expand Down Expand Up @@ -64,4 +65,29 @@ describe('ProvisioningFormPolicyContainer', () => {
expect(screen.getByText(INITIAL_CATALOG_QUERIES.multipleQueries[0].catalogQueryTitle)).toBeTruthy();
expect(screen.getByText(INITIAL_CATALOG_QUERIES.multipleQueries[1].catalogQueryTitle)).toBeTruthy();
});
it('renders policy type and selects AssignedLearnerCreditAccessPolicy', async () => {
const updatedInitialState = {
...initialStateValue,
multipleFunds: false,
formData: {
...initialStateValue.formData,
policies: INITIAL_CATALOG_QUERIES.defaultQuery,
},
};
renderWithRouter(
<ProvisioningFormPolicyContainerWrapper
value={updatedInitialState}
sampleCatalogQuery={INITIAL_CATALOG_QUERIES.defaultQuery}
/>,
);
expect(screen.getByText(POLICY_TYPE.TITLE)).toBeTruthy();
expect(screen.getByText(POLICY_TYPE.LABEL)).toBeTruthy();
const learnerOption = screen.getByTestId(POLICY_TYPE.OPTIONS.ADMIN_SELECTS.DESCRIPTION);
await act(async () => {
userEvent.click(learnerOption);
});
await waitFor(() => {
expect(learnerOption.checked).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable react/prop-types */
import { renderWithRouter } from '@edx/frontend-enterprise-utils';
import { fireEvent, screen } from '@testing-library/react';
import { ProvisioningContext, initialStateValue } from '../../../../testData/Provisioning';
import PROVISIONING_PAGE_TEXT, { INITIAL_CATALOG_QUERIES } from '../../../data/constants';
import ProvisioningFormPolicyType from '../ProvisioningFormPolicyType';

const { POLICY_TYPE } = PROVISIONING_PAGE_TEXT.FORM;

const ProvisioningFormPolicyTypeWrapper = ({
value = initialStateValue,
index = 0,
}) => (
<ProvisioningContext value={value}>
<ProvisioningFormPolicyType index={index} />
</ProvisioningContext>
);

describe('ProvisioningFormPolicyType', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('renders', () => {
const updatedInitialState = {
...initialStateValue,
multipleFunds: false,
formData: {
...initialStateValue.formData,
policies: INITIAL_CATALOG_QUERIES.defaultQuery,
},
};
renderWithRouter(
<ProvisioningFormPolicyTypeWrapper
value={updatedInitialState}
index={0}
/>,
);

expect(screen.getByText(POLICY_TYPE.TITLE)).toBeTruthy();
expect(screen.getByText(POLICY_TYPE.LABEL)).toBeTruthy();

const policyTypeOptions = Object.keys(POLICY_TYPE.OPTIONS);
const policyTypeButtons = [];
// Retrieves a list of input elements based on test ids
for (let i = 0; i < policyTypeOptions.length; i++) {
policyTypeButtons.push(screen.getByTestId(POLICY_TYPE.OPTIONS[policyTypeOptions[i]].DESCRIPTION));
}

// Clicks on each input element and checks if it is checked
for (let i = 0; i < policyTypeButtons.length; i++) {
fireEvent.click(policyTypeButtons[i]);
expect(policyTypeButtons[i].checked).toBeTruthy();
}
});
});
17 changes: 17 additions & 0 deletions src/Configuration/Provisioning/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,23 @@ const PROVISIONING_PAGE_TEXT = {
},
},
},
POLICY_TYPE: {
TITLE: 'Budget distribution mode',
LABEL: 'How is content selected?',
OPTIONS: {
LEARNER_SELECTS: {
DESCRIPTION: 'Learner selects content or LMS',
VALUE: 'PerLearnerSpendCreditAccessPolicy',
ACCESS_METHOD: 'direct',
},
ADMIN_SELECTS: {
DESCRIPTION: 'Admin selects content',
VALUE: 'AssignedLearnerCreditAccessPolicy',
ACCESS_METHOD: 'assigned',
},
},
ERROR: 'Please select an option.',
},
},
};

Expand Down
3 changes: 3 additions & 0 deletions src/Configuration/Provisioning/data/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export default function useProvisioningContext() {

const setPerLearnerCap = (perLearnerCapAmount, index) => updateFormDataState(perLearnerCapAmount, true, index);

const setPolicyType = (policyType, index) => updateFormDataState(policyType, true, index);

const setHasEdits = (hasEditsBoolean) => updateRootDataState({ hasEdits: hasEditsBoolean });

const hydrateCatalogQueryData = useCallback(async () => {
Expand Down Expand Up @@ -376,5 +378,6 @@ export default function useProvisioningContext() {
setInvalidPolicyFields,
resetInvalidFields,
setHasEdits,
setPolicyType,
};
}
1 change: 1 addition & 0 deletions src/Configuration/Provisioning/data/tests/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ describe('determineInvalidFields', () => {
catalogQueryMetadata: false,
perLearnerCap: false,
perLearnerCapAmount: false,
policyType: false,
}]];
const emptyPolicyDataset = {
...emptyDataSet,
Expand Down
Loading

0 comments on commit 6edeec1

Please sign in to comment.