Skip to content

Commit

Permalink
Merge pull request #364 from openedx/pwnage101/ENT-7922
Browse files Browse the repository at this point in the history
feat: LC provisioning: unique/curated catalogs
  • Loading branch information
pwnage101 authored Dec 4, 2023
2 parents 6edeec1 + a8483c1 commit 750574d
Show file tree
Hide file tree
Showing 68 changed files with 1,657 additions and 1,611 deletions.
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ temp/babel-plugin-react-intl
### pyenv ###
.python-version

### Emacs ###
# IDEs and text editors
*~
/temp
/.vscode
.projectile
*.swp
.idea/
.project
.pycharm_helpers/
.pydevproject
.vscode/

### Local dev related ###
.env.private
Expand Down
2 changes: 1 addition & 1 deletion src/Configuration/Provisioning/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Dashboard = () => {
}
navigate({ ...location, state: newState, replace: true });
}
}, [toastText.successfulPlanCreation, toastText.successfulPlanSaved, location, locationState]);
}, [location, locationState]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getConfig } from '@edx/frontend-platform';
import { DjangoShort } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import ROUTES from '../../../data/constants/routes';
import { DJANGO_ADMIN_RETRIEVE_SUBSIDY_PATH } from '../data/constants';

const { HOME } = ROUTES.CONFIGURATION.SUB_DIRECTORY.PROVISIONING;
const dashboardLink = (planRowUuid, title) => {
Expand Down Expand Up @@ -33,7 +34,7 @@ export const DjangoIconHyperlink = ({ row }) => {
return (
<Hyperlink
key="django-icon"
destination={`${DJANGO_ADMIN_SUBSIDY_BASE_URL}/admin/subsidy/subsidy/?uuid=${rowUuid}`}
destination={`${DJANGO_ADMIN_SUBSIDY_BASE_URL}${DJANGO_ADMIN_RETRIEVE_SUBSIDY_PATH(rowUuid)}`}
target="_blank"
showLaunchIcon={false}
data-testid="django-admin-link"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('DashboardTableLinks', () => {
DJANGO_ADMIN_SUBSIDY_BASE_URL: 'https://pokemons.com',
}));
render(<DjangoIconHyperlink row={row} />);
expect(screen.getByTestId('django-admin-link')).toHaveAttribute('href', 'https://pokemons.com/admin/subsidy/subsidy/?uuid=123456789');
expect(screen.getByTestId('django-admin-link')).toHaveAttribute('href', 'https://pokemons.com/admin/subsidy/subsidy/123456789/change/');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ErrorPageContainer = ({ to }) => {
const newState = { ...locationState };
delete newState.errorMessage;
navigate({ ...location, state: newState, replace: true });
}, [locationState?.errorMessage]);
}, [location, locationState, navigate]);

return (
<Container size="md" className="mt-5 text-center">
Expand Down
18 changes: 15 additions & 3 deletions src/Configuration/Provisioning/ProvisioningContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,34 @@ export const ProvisioningContext = createContext(null);
const ProvisioningContextProvider = ({ children }) => {
const contextValue = useState({
customers: [],
// `multipleFunds` = true means the user has selected the option to split the spend into two budgets.
multipleFunds: undefined,
customCatalog: false,
alertMessage: undefined,
catalogQueries: {
// `existingEnterpriseCatalogs` is a local cache of all existing catalogs belonging to the enterprise customer which
// pertains to this subsidy/plan. When the custom/unique/curated catalog option is selected, values in this object
// will appear in a drop-down list for the user to select.
existingEnterpriseCatalogs: {
data: [],
isLoading: true,
},
formData: {
// `policies` is a list of all policies/budgets inputs for this plan. After form completion, length should be 1 if
// multipleFunds = false, and 2 and multipleFunds = true.
policies: [],
},
// `showInvalidField` indicates when form fields are valid/invalid, possibly triggering invalid attributes to be set
// on various form field containers.
showInvalidField: {
subsidy: [],
// `subsidy` contains a mapping of subsidy form fields to boolean (true means valid).
subsidy: {},
// `policies` contains a list of mappings of policy form fields to boolean (true means valid), one mapping per
// policy.
policies: [],
},
// `isEditMode` = true means we're viewing a plan, and we're allowed to edit it.
isEditMode: false,
isLoading: true,
// `hasEdits` = true means we're editing a plan, and at least one field has been edited.
hasEdits: false,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
import { useEffect, useState } from 'react';
import ProvisioningFormDefineCustomCatalogHeader from './ProvisioningFormDefineCustomCatalogHeader';
import ProvisioningFormCustomCatalogDropdown from './ProvisioningFormCustomCatalogDropdown';
import ProvisioningFormCustomCatalogTitle from './ProvisioningFormCustomCatalogTitle';
import ProvisioningFormCustomCatalogTextArea from './ProvisioningFormCustomCatalogTextArea';
import { indexOnlyPropType, selectProvisioningContext } from '../../data/utils';
import ProvisioningFormCustomCatalogExecEdBoolean from './ProvisioningFormCustomCatalogExecEdBoolean';

const ProvisioningFormCustomCatalog = ({ index }) => {
const [formData] = selectProvisioningContext('formData');
const [policyData, setPolicyData] = useState(formData.policies[index].catalogQueryMetadata);

useEffect(() => {
setPolicyData(formData.policies[index].catalogQueryMetadata);
}, [formData.policies[index].catalogQueryMetadata, formData.policies[index].customerCatalog]);

return (
<article className="mt-4.5">
<ProvisioningFormDefineCustomCatalogHeader index={index} />
<ProvisioningFormCustomCatalogDropdown />
{policyData.catalogQuery.title && <ProvisioningFormCustomCatalogTitle />}
{policyData.catalogQuery.contentFilter && <ProvisioningFormCustomCatalogTextArea />}
{(policyData.catalogQuery.includeExecEd2UCourses !== undefined)
&& <ProvisioningFormCustomCatalogExecEdBoolean />}
</article>
);
};
import { indexOnlyPropType } from '../../data/utils';

const ProvisioningFormCustomCatalog = ({ index }) => (
<article className="mt-4.5">
<ProvisioningFormDefineCustomCatalogHeader index={index} />
<ProvisioningFormCustomCatalogDropdown />
</article>
);

ProvisioningFormCustomCatalog.propTypes = indexOnlyPropType;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,103 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Form, Button } from '@edx/paragon';
import React, { useCallback, useEffect } from 'react';
import { Button, Form } from '@edx/paragon';
import { v4 as uuidv4 } from 'uuid';
import { logError } from '@edx/frontend-platform/logging';
import PROVISIONING_PAGE_TEXT from '../../data/constants';
import useProvisioningContext from '../../data/hooks';
import { selectProvisioningContext, sortedCatalogQueries } from '../../data/utils';
import { selectProvisioningContext, sortedCustomCatalogs } from '../../data/utils';

const ProvisioningFormCustomCatalogDropdown = () => {
const [catalogQueries, showInvalidField, isEditMode, customCatalog, formData] = selectProvisioningContext('catalogQueries', 'showInvalidField', 'isEditMode', 'customCatalog', 'formData');
const { hydrateCatalogQueryData, setCatalogQueryCategory, setInvalidPolicyFields } = useProvisioningContext();
const [
existingEnterpriseCatalogs,
showInvalidField,
isEditMode,
formData,
] = selectProvisioningContext(
'existingEnterpriseCatalogs',
'showInvalidField',
'isEditMode',
'formData',
);
const {
hydrateEnterpriseCatalogsData,
setInvalidPolicyFields,
setPredefinedQueryType,
setCatalogUuid,
setCatalogTitle,
} = useProvisioningContext();
const { CUSTOM_CATALOG } = PROVISIONING_PAGE_TEXT.FORM;
let submittedFormCustomCatalogTitle;
if (isEditMode && customCatalog) {
const catalogTitle = formData.policies[0].catalogQueryMetadata.catalogQuery.title;
const catalogUuid = formData.policies[0].catalogQueryMetadata.catalogQuery.uuid;

// TODO: In the future the index will have to be brought in for custom catalogs.
const index = 0;
if (isEditMode && formData.policies[index].oldCustomCatalog) {
const { catalogTitle } = formData.policies[index];
const { catalogUuid } = formData.policies[index];
submittedFormCustomCatalogTitle = `${catalogTitle} --- ${catalogUuid}`;
if (!catalogTitle && !catalogUuid) {
submittedFormCustomCatalogTitle = CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.title;
}
}
const hydrateEnterpriseCatalogsDataCallback = useCallback(async () => {
try {
await hydrateEnterpriseCatalogsData(formData.enterpriseUUID);
} catch (e) {
logError(e);
}
}, [hydrateEnterpriseCatalogsData, formData.enterpriseUUID]);

useEffect(() => {
hydrateCatalogQueryData();
}, []);
const handleClick = useCallback((e) => {
const { currentTarget } = e;
setPredefinedQueryType(undefined, index);
setCatalogUuid(currentTarget.getAttribute('data-cataloguuid'), index);
setCatalogTitle(currentTarget.getAttribute('data-catalogtitle'), index);
setInvalidPolicyFields({ predefinedQueryType: true }, index);
}, [setCatalogTitle, setCatalogUuid, setInvalidPolicyFields, setPredefinedQueryType]);

const [selected, setSelected] = useState({ title: submittedFormCustomCatalogTitle || '' });
const { policies } = showInvalidField;
const isCatalogQueryMetadataDefinedAndFalse = policies[0]?.catalogQueryMetadata === false;
const isFormFieldInvalid = policies[index]?.catalogUuid === false;
const customCatalogs = sortedCustomCatalogs(existingEnterpriseCatalogs.data);
const isFormFieldReadonly = customCatalogs.length === 0;

const generateAutosuggestOptions = useCallback(() => {
const defaultDropdown = (
<Form.AutosuggestOption key={uuidv4()}>
Loading
</Form.AutosuggestOption>
);
if (catalogQueries.data.length > 0) {
const sortedData = sortedCatalogQueries(catalogQueries.data);
const apiCatalogQueries = sortedData.map(
/*
* generateAutosuggestOptions supports cases where the catalogs are still loading, OR none were found. In these
* cases, the user messaging is simply displayed as the title of a sole option.
*/
if (existingEnterpriseCatalogs.isLoading) {
const loadingDropdownOptions = (
<Form.AutosuggestOption key={uuidv4()}>
Loading...
</Form.AutosuggestOption>
);
return loadingDropdownOptions;
}
if (customCatalogs.length > 0) {
// * Filter/sort catalogs to show only custom catalogs and most recently modified ones first.
// * Use onClick on each option INSTEAD OF onSelected on the top-level Autosuggest because only onClick gives us
// the full event containing the custom data-* values.
const autoSuggestOptions = customCatalogs.map(
({ title, uuid }) => (
<Form.AutosuggestOption key={uuid}>
<Form.AutosuggestOption
key={uuid}
data-cataloguuid={uuid}
data-catalogtitle={title}
onClick={handleClick}
>
{`${title} --- ${uuid}`}
</Form.AutosuggestOption>
),
);
return apiCatalogQueries;
return autoSuggestOptions;
}
return defaultDropdown;
});
const handleOnSelected = (value) => {
// TODO: In the future the index will have to be brought in for custom catalogs per group
if (value) {
const valueUuid = value.split(' --- ')[1].trim();
setCatalogQueryCategory({
catalogQueryMetadata: {
catalogQuery: catalogQueries.data.find(({ uuid }) => uuid === valueUuid),
},
}, 0);
setInvalidPolicyFields({ catalogQueryMetadata: true }, 0);
}
setSelected(prevState => ({ selected: { ...prevState.selected, title: value } }));
};
const noCatalogsDropdownOptions = (
<Form.AutosuggestOption key={uuidv4()}>
No catalogs found for customer.
</Form.AutosuggestOption>
);
return noCatalogsDropdownOptions;
}, [existingEnterpriseCatalogs.isLoading, customCatalogs, handleClick]);

useEffect(() => {
hydrateEnterpriseCatalogsDataCallback();
}, [hydrateEnterpriseCatalogsDataCallback]);

return (
<div className="row">
Expand All @@ -66,27 +106,27 @@ const ProvisioningFormCustomCatalogDropdown = () => {
className="mt-4.5"
>
<Form.Autosuggest
floatingLabel={CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.title}
helpMessage={CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.subtitle}
value={selected.title}
onSelected={handleOnSelected}
floatingLabel={CUSTOM_CATALOG.OPTIONS.enterpriseCatalog.title}
helpMessage={CUSTOM_CATALOG.OPTIONS.enterpriseCatalog.subtitle}
value={submittedFormCustomCatalogTitle || ''}
data-testid="custom-catalog-dropdown-autosuggest"
isInvalid={isCatalogQueryMetadataDefinedAndFalse}
isInvalid={isFormFieldInvalid}
readOnly={isFormFieldReadonly}
>
{generateAutosuggestOptions()}
</Form.Autosuggest>
{isCatalogQueryMetadataDefinedAndFalse && (
{isFormFieldInvalid && (
<Form.Control.Feedback
type="invalid"
>
{CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.error}
{CUSTOM_CATALOG.OPTIONS.enterpriseCatalog.error}
</Form.Control.Feedback>
)}
</Form.Group>
</div>
{/* TODO: Button should be removed in favor of react-query's refetch functionality */}
<div className="col-2 align-self-center mb-3">
<Button onClick={hydrateCatalogQueryData}>Refresh</Button>
<Button onClick={hydrateEnterpriseCatalogsDataCallback}>Refresh</Button>
</div>
</div>
);
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit 750574d

Please sign in to comment.