├── .dockerignore ├── .env ├── .env.development ├── .env.development-stage ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── add-depr-ticket-to-depr-board.yml │ ├── add-remove-label-on-comment.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── lockfileversion-check.yml │ ├── self-assign-issue.yml │ └── update-browserslist-db.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── AUTHORS ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── __mocks__ ├── fileMock.js └── react-instantsearch-dom.jsx ├── catalog-info.yaml ├── docker-compose.yml ├── docs ├── decisions │ ├── 0001-record-architecture-decisions.rst │ ├── 0002-system-wide-banner-configuration.rst │ ├── 0003-learner-credit-management.rst │ ├── 0004-learner-credit-data-from-analytics-service.rst │ ├── 0005-moment-to-dayjs.rst │ ├── 0006-tanstack-react-query.rst │ ├── 0007-patch-package.rst │ ├── 0008-application_state_differenciation.rst │ ├── 0009-useCache_deprecation.rst │ ├── 0010-algolia-filters-catalog-query-uuids.rst │ └── 0011-typescript-migration-over-time.rst └── how_tos │ └── i18n.rst ├── jest.config.js ├── jsdom-with-global.js ├── package-lock.json ├── package.json ├── packages └── dash-embedded-component-2.0.2.tgz ├── public └── index.html ├── renovate.json ├── src ├── algoliaUtils │ ├── algoliaUtils.test.js │ └── index.js ├── colors.scss ├── components │ ├── AIAnalyticsSummary │ │ ├── data │ │ │ └── hooks.js │ │ └── tests │ │ │ └── hooks.test.js │ ├── ActionButtonWithModal │ │ └── index.jsx │ ├── Admin │ │ ├── AIAnalyticsSummary.jsx │ │ ├── AIAnalyticsSummary.test.jsx │ │ ├── AIAnalyticsSummarySkeleton.jsx │ │ ├── Admin.test.jsx │ │ ├── AdminCards.jsx │ │ ├── AdminCardsSkeleton.jsx │ │ ├── AdminCardsSkeleton.test.jsx │ │ ├── AdminSearchForm.jsx │ │ ├── AdminSearchForm.test.jsx │ │ ├── EmbeddedSubscription.jsx │ │ ├── EmbeddedSubscription.test.jsx │ │ ├── SubscriptionDetailPage.jsx │ │ ├── SubscriptionDetailPage.test.jsx │ │ ├── SubscriptionDetails.jsx │ │ ├── SubscriptionDetails.test.jsx │ │ ├── _Admin.scss │ │ ├── __snapshots__ │ │ │ ├── AIAnalyticsSummary.test.jsx.snap │ │ │ ├── Admin.test.jsx.snap │ │ │ └── AdminCardsSkeleton.test.jsx.snap │ │ ├── data │ │ │ └── hooks │ │ │ │ └── index.js │ │ ├── index.jsx │ │ ├── licenses │ │ │ ├── LicenseAllocationDetails.jsx │ │ │ ├── LicenseAllocationDetails.test.jsx │ │ │ ├── LicenseAllocationHeader.jsx │ │ │ ├── LicenseAllocationHeader.test.jsx │ │ │ ├── LicenseManagementTable │ │ │ │ ├── index.jsx │ │ │ │ └── index.test.jsx │ │ │ └── __snapshots__ │ │ │ │ └── LicenseAllocationHeader.test.jsx.snap │ │ └── tabs │ │ │ ├── DownloadCSVButton.jsx │ │ │ ├── DownloadCsvButton.test.jsx │ │ │ ├── ModuleActivityReport.jsx │ │ │ └── ModuleActivityReport.test.jsx │ ├── AdminRegisterPage │ │ ├── AdminRegisterPage.test.jsx │ │ └── index.jsx │ ├── AdminV2 │ │ ├── AIAnalyticsSummary.jsx │ │ ├── AIAnalyticsSummarySkeleton.jsx │ │ ├── AdminCards.jsx │ │ ├── AdminCardsSkeleton.jsx │ │ ├── AdminSearchForm.jsx │ │ ├── AnalyticsOverview.jsx │ │ ├── LearnerReport.jsx │ │ ├── SortableItem.jsx │ │ ├── SubscriptionModal.jsx │ │ ├── _Admin.scss │ │ ├── cards │ │ │ └── NumberCard │ │ │ │ ├── DetailsAction.jsx │ │ │ │ ├── DetailsAction.test.jsx │ │ │ │ ├── NumberCard.test.jsx │ │ │ │ └── index.jsx │ │ ├── data │ │ │ └── hooks │ │ │ │ └── index.js │ │ ├── index.jsx │ │ ├── tabs │ │ │ ├── DownloadCSVButton.jsx │ │ │ ├── DownloadCsvButton.test.jsx │ │ │ ├── ModuleActivityReport.jsx │ │ │ └── ModuleActivityReport.test.jsx │ │ └── tests │ │ │ ├── AIAnalyticsSummary.test.jsx │ │ │ ├── Admin.test.jsx │ │ │ ├── AdminCards.test.jsx │ │ │ ├── AdminCardsSkeleton.test.jsx │ │ │ ├── AdminSearchForm.test.jsx │ │ │ ├── SortableItem.test.jsx │ │ │ └── SubscriptionModal.test.jsx │ ├── AdvanceAnalyticsV2 │ │ ├── AnalyticsV2Page.jsx │ │ ├── DownloadCSVButton.jsx │ │ ├── DownloadCSVButton.test.jsx │ │ ├── Header.jsx │ │ ├── ProgressOverlay.jsx │ │ ├── Stats.jsx │ │ ├── charts │ │ │ ├── BarChart.jsx │ │ │ ├── BarChart.test.jsx │ │ │ ├── ChartWrapper.jsx │ │ │ ├── EmptyChart.jsx │ │ │ ├── EmptyChart.test.jsx │ │ │ ├── LineChart.jsx │ │ │ ├── LineChart.test.jsx │ │ │ ├── ScatterChart.jsx │ │ │ └── ScatterChart.test.jsx │ │ ├── constants.js │ │ ├── data │ │ │ ├── constants.js │ │ │ ├── hooks.test.jsx │ │ │ ├── hooks │ │ │ │ ├── index.js │ │ │ │ ├── useEnterpriseCompletionsData.js │ │ │ │ ├── useEnterpriseEngagementData.js │ │ │ │ └── useEnterpriseEnrollmentsData.js │ │ │ ├── utils.js │ │ │ └── utils.test.js │ │ ├── messages.js │ │ ├── styles │ │ │ └── index.scss │ │ ├── tabs │ │ │ ├── AnalyticsTable.jsx │ │ │ ├── Completions.jsx │ │ │ ├── Completions.test.jsx │ │ │ ├── Engagements.jsx │ │ │ ├── Engagements.test.jsx │ │ │ ├── Enrollments.jsx │ │ │ ├── Enrollments.test.jsx │ │ │ ├── Leaderboard.jsx │ │ │ ├── Leaderboard.test.jsx │ │ │ ├── Skills.jsx │ │ │ └── Skills.test.jsx │ │ └── tests │ │ │ ├── AnalyticsV2Page.test.jsx │ │ │ ├── Header.test.jsx │ │ │ └── Stats.test.jsx │ ├── App │ │ └── index.jsx │ ├── AuthenticatedEnterpriseApp │ │ └── index.jsx │ ├── BrandStyles │ │ └── index.jsx │ ├── BudgetExpiryAlertAndModal │ │ ├── data │ │ │ ├── constants.js │ │ │ ├── expiryThresholds.js │ │ │ ├── hooks │ │ │ │ ├── useExpiry.jsx │ │ │ │ └── useExpiry.test.jsx │ │ │ ├── index.test.jsx │ │ │ └── utils.js │ │ └── index.jsx │ ├── BulkEnrollmentPage │ │ ├── BulkEnrollButton.jsx │ │ ├── BulkEnrollDialog.jsx │ │ ├── BulkEnrollment.scss │ │ ├── BulkEnrollmentContext.tsx │ │ ├── BulkEnrollmentWarningModal.jsx │ │ ├── CourseSearchResults.jsx │ │ ├── CourseSearchResults.test.jsx │ │ ├── data │ │ │ ├── actions.test.js │ │ │ ├── actions.ts │ │ │ ├── constants.ts │ │ │ ├── reducer.test.js │ │ │ ├── reducer.ts │ │ │ └── types.ts │ │ ├── stepper │ │ │ ├── AddCoursesStep.test.tsx │ │ │ ├── AddCoursesStep.tsx │ │ │ ├── BulkEnrollmentStepper.jsx │ │ │ ├── BulkEnrollmentSubmit.jsx │ │ │ ├── BulkEnrollmentSubmit.test.jsx │ │ │ ├── DismissibleCourseWarning.jsx │ │ │ ├── ReviewItem.jsx │ │ │ ├── ReviewItem.test.jsx │ │ │ ├── ReviewList.jsx │ │ │ ├── ReviewList.test.jsx │ │ │ ├── ReviewStep.jsx │ │ │ ├── ReviewStepCourseList.test.tsx │ │ │ ├── ReviewStepCourseList.tsx │ │ │ └── constants.jsx │ │ └── table │ │ │ ├── BaseSelectionStatus.test.jsx │ │ │ ├── BaseSelectionStatus.tsx │ │ │ ├── BulkEnrollSelect.jsx │ │ │ ├── BulkEnrollSelect.test.jsx │ │ │ ├── CourseSearchResultsCells.jsx │ │ │ └── CourseSearchResultsCells.test.jsx │ ├── BulkEnrollmentResultsDownloadPage │ │ ├── BulkEnrollmentResultsDownloadPage.test.jsx │ │ └── index.jsx │ ├── CodeAssignmentModal │ │ ├── BulkAssignFields.jsx │ │ ├── CodeAssignmentModal.scss │ │ ├── CodeAssignmentModal.test.jsx │ │ ├── IndividualAssignFields.jsx │ │ ├── constants.jsx │ │ ├── emailTemplate.js │ │ ├── index.jsx │ │ ├── messages.js │ │ ├── validation.js │ │ └── validation.test.js │ ├── CodeManagement │ │ ├── CodeManagementRoutes.jsx │ │ ├── CouponCodeTabs.jsx │ │ ├── ManageCodesTab.jsx │ │ ├── ManageRequestsTab.jsx │ │ ├── data │ │ │ └── constants.js │ │ ├── index.jsx │ │ └── tests │ │ │ ├── CodeManagementRoutes.test.jsx │ │ │ ├── CouponCodeTabs.test.jsx │ │ │ ├── ManageCodesTab.test.jsx │ │ │ └── ManageRequestsTab.test.jsx │ ├── CodeModal │ │ ├── ModalError.jsx │ │ ├── ModalError.test.jsx │ │ ├── codeModalHelpers.js │ │ └── index.jsx │ ├── CodeReminderModal │ │ ├── CodeDetails.jsx │ │ ├── CodeReminderModal.scss │ │ ├── CodeReminderModal.test.jsx │ │ ├── emailTemplate.js │ │ └── index.jsx │ ├── CodeRevokeModal │ │ ├── CodeRevokeModal.test.jsx │ │ ├── emailTemplate.js │ │ └── index.jsx │ ├── CodeSearchResults │ │ ├── CodeSearchResults.test.jsx │ │ ├── CodeSearchResultsHeading.jsx │ │ ├── CodeSearchResultsTable.jsx │ │ ├── _CodeSearchResults.scss │ │ ├── __snapshots__ │ │ │ └── CodeSearchResults.test.jsx.snap │ │ └── index.jsx │ ├── CompletedLearnersTable │ │ ├── CompletedLearnersTable.test.jsx │ │ ├── __snapshots__ │ │ │ └── CompletedLearnersTable.test.jsx.snap │ │ └── index.jsx │ ├── ConfirmationModal │ │ ├── ConfirmationModal.test.jsx │ │ └── index.jsx │ ├── ContactCustomerSupportButton │ │ └── index.jsx │ ├── ContentHighlights │ │ ├── CatalogVisibility │ │ │ ├── ContentHighlightCatalogVisibility.jsx │ │ │ ├── ContentHighlightCatalogVisibilityAlert.jsx │ │ │ ├── ContentHighlightCatalogVisibilityHeader.jsx │ │ │ ├── ContentHighlightCatalogVisibilityRadioInput.jsx │ │ │ ├── index.js │ │ │ └── tests │ │ │ │ ├── ContentHighlightCatalogVisibilityAlert.test.jsx │ │ │ │ └── ContentHighlightCatalogVisibilityRadioInput.test.jsx │ │ ├── ContentHighlightArchivedAlert.jsx │ │ ├── ContentHighlightCardContainer.jsx │ │ ├── ContentHighlightCardItem.jsx │ │ ├── ContentHighlightHelmet.jsx │ │ ├── ContentHighlightRoutes.jsx │ │ ├── ContentHighlightSet.jsx │ │ ├── ContentHighlightSetCard.jsx │ │ ├── ContentHighlightToast.jsx │ │ ├── ContentHighlights.tsx │ │ ├── ContentHighlightsCardItemsContainer.jsx │ │ ├── ContentHighlightsContext.tsx │ │ ├── ContentHighlightsDashboard.jsx │ │ ├── CurrentContentHighlightHeader.jsx │ │ ├── CurrentContentHighlightItemsHeader.jsx │ │ ├── CurrentContentHighlights.jsx │ │ ├── DeleteArchivedHighlightsDialogs.jsx │ │ ├── DeleteHighlightSet.jsx │ │ ├── HighlightSetSection.jsx │ │ ├── HighlightStepper │ │ │ ├── ContentConfirmContentCard.jsx │ │ │ ├── ContentHighlightStepper.jsx │ │ │ ├── ContentSearchResultCard.jsx │ │ │ ├── HighlightStepperConfirmContent.tsx │ │ │ ├── HighlightStepperFooterHelpLink.jsx │ │ │ ├── HighlightStepperSelectContent.jsx │ │ │ ├── HighlightStepperSelectContentHeader.jsx │ │ │ ├── HighlightStepperSelectContentSearch.tsx │ │ │ ├── HighlightStepperTitle.jsx │ │ │ ├── HighlightStepperTitleInput.jsx │ │ │ ├── SelectContentSearchPagination.jsx │ │ │ ├── SelectContentSelectionCheckbox.jsx │ │ │ ├── SelectContentSelectionStatus.jsx │ │ │ └── tests │ │ │ │ ├── ContentConfirmContentCard.test.jsx │ │ │ │ ├── ContentHighlightStepper.test.jsx │ │ │ │ ├── HighlightStepperConfirmContent.test.jsx │ │ │ │ └── HighlightStepperSelectContentSearch.test.jsx │ │ ├── SkeletonContentCard.jsx │ │ ├── SkeletonContentCardContainer.jsx │ │ ├── ZeroState │ │ │ ├── ZeroStateCardFooter.jsx │ │ │ ├── ZeroStateCardImage.jsx │ │ │ ├── ZeroStateCardText.jsx │ │ │ ├── ZeroStateHighlights.jsx │ │ │ └── index.js │ │ ├── data │ │ │ ├── constants.js │ │ │ ├── hooks.js │ │ │ ├── images │ │ │ │ └── ContentHighlightImage.svg │ │ │ ├── tests │ │ │ │ └── constants.test.js │ │ │ └── utils.js │ │ ├── index.js │ │ └── tests │ │ │ ├── ContentHighlightCardItem.test.jsx │ │ │ ├── ContentHighlightSet.test.jsx │ │ │ ├── ContentHighlightSetCard.test.jsx │ │ │ ├── ContentHighlightToast.test.jsx │ │ │ ├── ContentHighlights.test.tsx │ │ │ ├── ContentHighlightsCardItemsContainer.test.jsx │ │ │ ├── ContentHighlightsDashboard.test.jsx │ │ │ ├── CurrentContentHighlightItemsHeader.test.jsx │ │ │ ├── CurrentContentHighlights.test.jsx │ │ │ ├── DeleteArchivedCourses.test.jsx │ │ │ ├── DeleteHighlightSet.test.jsx │ │ │ └── HighlightSetSection.test.jsx │ ├── Coupon │ │ ├── Coupon.test.jsx │ │ ├── _Coupon.scss │ │ ├── __snapshots__ │ │ │ └── Coupon.test.jsx.snap │ │ └── index.jsx │ ├── CouponDetails │ │ ├── ActionButton.jsx │ │ ├── ActionButton.test.jsx │ │ ├── CouponBulkActions.jsx │ │ ├── CouponBulkActions.test.jsx │ │ ├── CouponFilters.jsx │ │ ├── FilterBulkActionRow.jsx │ │ ├── _CouponDetails.scss │ │ ├── constants.js │ │ ├── helpers.js │ │ ├── helpers.test.jsx │ │ ├── index.jsx │ │ └── index.test.jsx │ ├── DownloadCsvButton │ │ └── index.jsx │ ├── EmailTemplateForm │ │ ├── EmailTemplateForm.test.jsx │ │ ├── constants.js │ │ ├── index.jsx │ │ └── messages.js │ ├── EnrolledLearnersForInactiveCoursesTable │ │ ├── EnrolledLearnersForInactiveCoursesTable.test.jsx │ │ ├── __snapshots__ │ │ │ └── EnrolledLearnersForInactiveCoursesTable.test.jsx.snap │ │ └── index.jsx │ ├── EnrolledLearnersTable │ │ ├── EnrolledLearnersTable.test.jsx │ │ ├── __snapshots__ │ │ │ └── EnrolledLearnersTable.test.jsx.snap │ │ └── index.jsx │ ├── EnrollmentsTable │ │ ├── EnrollmentsTable.mocks.js │ │ ├── EnrollmentsTable.test.jsx │ │ ├── __snapshots__ │ │ │ └── EnrollmentsTable.test.jsx.snap │ │ └── index.jsx │ ├── EnterpriseApp │ │ ├── EnterpriseApp.test.jsx │ │ ├── EnterpriseAppContent.jsx │ │ ├── EnterpriseAppContextProvider.test.tsx │ │ ├── EnterpriseAppContextProvider.tsx │ │ ├── EnterpriseAppRoutes.jsx │ │ ├── EnterpriseAppRoutes.test.jsx │ │ ├── EnterpriseAppSkeleton.jsx │ │ ├── EnterpriseAppSkeleton.test.jsx │ │ ├── _EnterpriseApp.scss │ │ ├── __snapshots__ │ │ │ └── EnterpriseAppSkeleton.test.jsx.snap │ │ ├── data │ │ │ ├── constants.js │ │ │ ├── enterpriseCurationReducer.js │ │ │ ├── enterpriseCurationReducer.test.js │ │ │ └── hooks │ │ │ │ ├── index.js │ │ │ │ ├── useEnterpriseCuration.js │ │ │ │ ├── useEnterpriseCuration.test.js │ │ │ │ ├── useEnterpriseCurationContext.js │ │ │ │ ├── useEnterpriseCurationContext.test.js │ │ │ │ ├── useUpdateActiveEnterpriseForUser.js │ │ │ │ └── useUpdateActiveEnterpriseForUser.test.jsx │ │ └── index.jsx │ ├── EnterpriseList │ │ ├── EnterpriseList.mocks.js │ │ ├── EnterpriseList.test.jsx │ │ └── index.jsx │ ├── EnterpriseSubsidiesContext │ │ ├── data │ │ │ ├── hooks.js │ │ │ └── tests │ │ │ │ ├── hooks.test.jsx │ │ │ │ └── index.test.js │ │ └── index.jsx │ ├── ErrorPage │ │ ├── ErrorPage.test.jsx │ │ ├── __snapshots__ │ │ │ └── ErrorPage.test.jsx.snap │ │ └── index.jsx │ ├── FeatureAnnouncementBanner │ │ ├── FeatureAnnouncementBanner.scss │ │ ├── FeatureAnnouncementBanner.test.jsx │ │ └── index.jsx │ ├── FeatureNotSupportedPage │ │ ├── FeatureNotSupportedPage.test.jsx │ │ ├── __snapshots__ │ │ │ └── FeatureNotSupportedPage.test.jsx.snap │ │ └── index.jsx │ ├── FileInput │ │ ├── _FileInput.scss │ │ └── index.jsx │ ├── FloatingCollapsible │ │ ├── _FloatingCollapsible.scss │ │ ├── index.test.jsx │ │ └── index.tsx │ ├── Footer │ │ ├── Footer.scss │ │ ├── index.jsx │ │ └── messages.js │ ├── ForbiddenPage │ │ ├── ForbiddenPage.test.jsx │ │ └── index.jsx │ ├── Header │ │ ├── Header.scss │ │ ├── Header.test.jsx │ │ └── index.jsx │ ├── Hero │ │ ├── Hero.test.jsx │ │ ├── _Hero.scss │ │ └── index.jsx │ ├── IconWithTooltip │ │ ├── IconWithTooltip.test.jsx │ │ └── index.jsx │ ├── Img │ │ ├── Img.scss │ │ └── index.jsx │ ├── InfoHover │ │ └── index.jsx │ ├── InviteLearnersModal │ │ ├── emailTemplate.js │ │ └── index.jsx │ ├── LearnerActivityTable │ │ ├── LearnerActivityTable.test.jsx │ │ ├── __snapshots__ │ │ │ └── LearnerActivityTable.test.jsx.snap │ │ └── index.jsx │ ├── LoadingMessage │ │ ├── _LoadingMessage.scss │ │ └── index.jsx │ ├── MultipleFileInputField │ │ ├── MultipleFileInputField.jsx │ │ ├── MultipleFileInputField.test.jsx │ │ ├── constants.js │ │ └── utils.js │ ├── NewFeatureAlertBrowseAndRequest │ │ ├── NewFeatureAlertBrowseAndRequest.test.jsx │ │ ├── data │ │ │ └── constants.js │ │ └── index.jsx │ ├── NotFoundPage │ │ ├── NotFoundPage.test.jsx │ │ ├── __snapshots__ │ │ │ └── NotFoundPage.test.jsx.snap │ │ └── index.jsx │ ├── NumberCard │ │ ├── NumberCard.test.jsx │ │ ├── _NumberCard.scss │ │ ├── __snapshots__ │ │ │ └── NumberCard.test.jsx.snap │ │ └── index.jsx │ ├── PastWeekPassedLearnersTable │ │ ├── PastWeekPassedLearnersTable.test.jsx │ │ ├── __snapshots__ │ │ │ └── PastWeekPassedLearnersTable.test.jsx.snap │ │ └── index.jsx │ ├── PeopleManagement │ │ ├── AddMembersModal │ │ │ ├── AddMemberModalSummaryDuplicate.jsx │ │ │ ├── AddMemberModalSummaryEmptyState.jsx │ │ │ ├── AddMemberModalSummaryErrorState.jsx │ │ │ ├── AddMemberModalSummaryLearnerList.jsx │ │ │ ├── AddMembersModal.tsx │ │ │ ├── AddMembersModalContent.jsx │ │ │ └── AddMembersModalSummary.jsx │ │ ├── CreateGroupModal.tsx │ │ ├── CreateGroupModalContent.jsx │ │ ├── DownloadCSVButton.jsx │ │ ├── EnterpriseCustomerUserDataTable.jsx │ │ ├── GeneralErrorModal.jsx │ │ ├── GroupCardGrid.jsx │ │ ├── GroupDetailCard.jsx │ │ ├── GroupDetailPage │ │ │ ├── AddMemberTableAction.jsx │ │ │ ├── DeleteGroupModal.jsx │ │ │ ├── DownloadCsvIconButton.jsx │ │ │ ├── EditGroupNameModal.jsx │ │ │ ├── GroupDetailPage.jsx │ │ │ ├── GroupMembersTable.jsx │ │ │ └── RemoveMemberModal.jsx │ │ ├── GroupInviteErrorToast.tsx │ │ ├── LearnerDetailPage │ │ │ ├── CourseEnrollments.jsx │ │ │ ├── EnrollmentCard.jsx │ │ │ ├── LearnerAccess.tsx │ │ │ ├── LearnerDetailGroupMemberships.tsx │ │ │ ├── LearnerDetailPage.jsx │ │ │ └── data │ │ │ │ ├── hooks.js │ │ │ │ └── hooks.test.js │ │ ├── MemberDetailsCell.jsx │ │ ├── MemberJoinedDateCell.jsx │ │ ├── OrgMemberCard.jsx │ │ ├── PeopleManagementTable.jsx │ │ ├── RecentActionTableCell.jsx │ │ ├── ZeroState.jsx │ │ ├── _PeopleManagement.scss │ │ ├── constants.js │ │ ├── data │ │ │ ├── ValidatedEmailsContext.tsx │ │ │ ├── ValidatedEmailsContextProvider.tsx │ │ │ ├── actions.ts │ │ │ ├── hooks │ │ │ │ ├── index.js │ │ │ │ ├── useAllEnterpriseGroupLearners.js │ │ │ │ ├── useEnterpriseCourseEnrollments.ts │ │ │ │ ├── useEnterpriseGroupLearnersTableData.js │ │ │ │ ├── useEnterpriseGroupMemberships.ts │ │ │ │ ├── useEnterpriseGroupUuid.js │ │ │ │ ├── useEnterpriseMembersTableData.js │ │ │ │ ├── useLearnerCreditPlans.ts │ │ │ │ └── useLearnerProfileView.ts │ │ │ └── reducer.ts │ │ ├── images │ │ │ └── ZeroStateImage.svg │ │ ├── index.jsx │ │ ├── tests │ │ │ ├── AddMembersModal.test.jsx │ │ │ ├── CourseEnrollments.test.jsx │ │ │ ├── CreateGroupModal.test.jsx │ │ │ ├── DownloadCsvButton.test.jsx │ │ │ ├── DownloadCsvIconButton.test.jsx │ │ │ ├── GroupDetailPage.test.jsx │ │ │ ├── GroupInviteErrorToast.test.jsx │ │ │ ├── LearnerAccess.test.jsx │ │ │ ├── LearnerDetailPage.test.jsx │ │ │ ├── PeopleManagementPage.test.jsx │ │ │ ├── useEnterpriseGroupLearnersTableData.test.jsx │ │ │ ├── useEnterpriseMembersTableData.test.jsx │ │ │ └── utils.test.jsx │ │ └── utils.ts │ ├── ProductTours │ │ ├── AdminOnboardingTours │ │ │ ├── AdminOnboardingTours.tsx │ │ │ ├── OnboardingSteps.tsx │ │ │ ├── OnboardingWelcomeModal.tsx │ │ │ ├── constants.js │ │ │ ├── messages.js │ │ │ ├── tests │ │ │ │ ├── AdminOnboardingTours.test.jsx │ │ │ │ ├── OnboardingSteps.test.jsx │ │ │ │ └── useLearnerProgressTour.test.jsx │ │ │ └── useLearnerProgressTour.tsx │ │ ├── CheckpointOverlay.tsx │ │ ├── ProductTours.jsx │ │ ├── TourCollapsible.tsx │ │ ├── _ProductTours.scss │ │ ├── browseAndRequestTour.jsx │ │ ├── constants.js │ │ ├── data │ │ │ ├── hooks.ts │ │ │ ├── images │ │ │ │ └── WelcomeModal.svg │ │ │ └── utils.js │ │ ├── highlightsTour.jsx │ │ ├── learnerCreditTour.jsx │ │ ├── learnerDetailPageTour.jsx │ │ ├── portalAppearanceTour.jsx │ │ └── tests │ │ │ ├── CheckpointOverlay.test.jsx │ │ │ ├── ProductTours.test.jsx │ │ │ └── TourCollapsible.test.jsx │ ├── ReduxFormCheckbox │ │ ├── CheckboxWithTooltip.jsx │ │ ├── CheckboxWithTooltip.scss │ │ ├── ReduxFormCheckbox.test.jsx │ │ ├── __snapshots__ │ │ │ └── ReduxFormCheckbox.test.jsx.snap │ │ └── index.jsx │ ├── ReduxFormSelect │ │ ├── ReduxFormSelect.test.jsx │ │ └── index.jsx │ ├── RegisteredLearnersTable │ │ ├── RegisteredLearnersTable.test.jsx │ │ ├── __snapshots__ │ │ │ └── RegisteredLearnersTable.test.jsx.snap │ │ └── index.jsx │ ├── RemindButton │ │ └── index.jsx │ ├── RenderField │ │ ├── RenderField.test.jsx │ │ └── index.jsx │ ├── ReportingConfig │ │ ├── EmailDeliveryMethodForm.jsx │ │ ├── ReportingConfigForm.jsx │ │ ├── ReportingConfigForm.test.jsx │ │ ├── SFTPDeliveryMethodForm.jsx │ │ ├── index.jsx │ │ └── index.test.jsx │ ├── RequestCodesPage │ │ ├── RequestCodesForm.jsx │ │ ├── RequestCodesForm.test.jsx │ │ ├── _RequestCodesPage.scss │ │ ├── index.jsx │ │ └── messages.js │ ├── RevokeButton │ │ └── index.jsx │ ├── SaveTemplateButton │ │ └── index.jsx │ ├── SearchBar │ │ ├── SearchBar.test.jsx │ │ ├── __snapshots__ │ │ │ └── SearchBar.test.jsx.snap │ │ └── index.jsx │ ├── Sidebar │ │ ├── IconLink.jsx │ │ ├── IconLink.test.jsx │ │ ├── _Sidebar.scss │ │ └── index.jsx │ ├── SidebarToggle │ │ ├── SidebarToggle.scss │ │ └── index.jsx │ ├── SubsidyRequestManagementTable │ │ ├── ActionCell.jsx │ │ ├── CourseTitleCell.jsx │ │ ├── EmailAddressCell.jsx │ │ ├── RequestDateCell.jsx │ │ ├── RequestStatusCell.jsx │ │ ├── SubsidyRequestManagementTable.jsx │ │ ├── data │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── hooks.js │ │ │ ├── reducer.js │ │ │ ├── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── hooks.test.js │ │ │ │ └── reducer.test.js │ │ │ └── utils.js │ │ ├── index.js │ │ └── tests │ │ │ ├── ActionCell.test.jsx │ │ │ ├── CourseTitleCell.test.jsx │ │ │ ├── EmailAddressCell.test.jsx │ │ │ ├── RequestDateCell.test.jsx │ │ │ ├── RequestStatusCell.test.jsx │ │ │ ├── SubsidyRequestManagementTable.test.jsx │ │ │ └── __snapshots__ │ │ │ ├── ActionCell.test.jsx.snap │ │ │ ├── EmailAddressCell.test.jsx.snap │ │ │ ├── RequestDateCell.test.jsx.snap │ │ │ ├── RequestStatusCell.test.jsx.snap │ │ │ └── SubsidyRequestManagementTable.test.jsx.snap │ ├── SurveyPage │ │ ├── SurveyPage.test.jsx │ │ ├── __snapshots__ │ │ │ └── SurveyPage.test.jsx.snap │ │ └── index.jsx │ ├── TableComponent │ │ ├── TableLoadingSkeleton.jsx │ │ ├── TableLoadingSkeleton.test.jsx │ │ ├── _TableComponent.scss │ │ ├── __snapshots__ │ │ │ └── TableLoadingSkeleton.test.jsx.snap │ │ ├── index.jsx │ │ └── index.test.jsx │ ├── TableLoadingOverlay │ │ ├── TableLoadingOverlay.test.jsx │ │ ├── _TableLoadingOverlay.scss │ │ ├── __snapshots__ │ │ │ └── TableLoadingOverlay.test.jsx.snap │ │ └── index.jsx │ ├── TemplateSourceFields │ │ ├── TemplateSourceFields.scss │ │ └── index.jsx │ ├── TextAreaAutoSize │ │ ├── TextAreaAutoSize.test.jsx │ │ └── index.jsx │ ├── UserActivationPage │ │ ├── UserActivationPage.test.jsx │ │ └── index.jsx │ ├── algolia-search │ │ ├── SearchUnavailableAlert.tsx │ │ ├── index.ts │ │ ├── useAlgoliaSearch.ts │ │ ├── withAlgoliaSearch.test.tsx │ │ └── withAlgoliaSearch.tsx │ ├── forms │ │ ├── FormContext.tsx │ │ ├── FormContextWrapper.tsx │ │ ├── FormWaitModal.tsx │ │ ├── FormWorkflow.tsx │ │ ├── ValidatedFormCheckbox.tsx │ │ ├── ValidatedFormControl.tsx │ │ ├── ValidatedFormRadio.tsx │ │ ├── _FormWorkflow.scss │ │ ├── data │ │ │ ├── actions.ts │ │ │ ├── reducer.test.ts │ │ │ └── reducer.ts │ │ └── tests │ │ │ ├── FormWaitModal.test.tsx │ │ │ ├── ValidatedFormControl.test.tsx │ │ │ └── ValidatedFormRadio.test.tsx │ ├── learner-credit-management │ │ ├── AssignMoreCoursesEmptyStateMinimal.jsx │ │ ├── AssignmentAmountTableCell.jsx │ │ ├── AssignmentDetailsTableCell.jsx │ │ ├── AssignmentEnrollByDateCell.jsx │ │ ├── AssignmentEnrollByDateHeader.jsx │ │ ├── AssignmentRecentActionTableCell.jsx │ │ ├── AssignmentRowActionTableCell.jsx │ │ ├── AssignmentStatusTableCell.jsx │ │ ├── AssignmentTableCancel.jsx │ │ ├── AssignmentTableRemind.jsx │ │ ├── AssignmentsTableRefreshAction.jsx │ │ ├── BudgetAssignmentsTable.jsx │ │ ├── BudgetCard.jsx │ │ ├── BudgetCheckboxFilter.jsx │ │ ├── BudgetDetail.jsx │ │ ├── BudgetDetailActivityTabContents.jsx │ │ ├── BudgetDetailAssignments.jsx │ │ ├── BudgetDetailCatalogTabContents.jsx │ │ ├── BudgetDetailPage.jsx │ │ ├── BudgetDetailPageBreadcrumbs.jsx │ │ ├── BudgetDetailPageHeader.jsx │ │ ├── BudgetDetailPageOverviewAvailability.jsx │ │ ├── BudgetDetailPageOverviewUtilization.jsx │ │ ├── BudgetDetailPageWrapper.jsx │ │ ├── BudgetDetailRedemptions.jsx │ │ ├── BudgetDetailRequestsTabContent.jsx │ │ ├── BudgetDetailTabsAndRoutes.jsx │ │ ├── BudgetOverviewContent.jsx │ │ ├── BudgetStatusSubtitle.jsx │ │ ├── CancelAssignmentModal.jsx │ │ ├── CustomDataTableEmptyState.jsx │ │ ├── EmailAddressTableCell.jsx │ │ ├── FlexGroupDropdown.jsx │ │ ├── LearnerCreditAggregateCards.jsx │ │ ├── LearnerCreditAllocationTable.jsx │ │ ├── LearnerCreditDisclaimer.jsx │ │ ├── MultipleBudgetsPage.jsx │ │ ├── MultipleBudgetsPicker.jsx │ │ ├── OfferDates.jsx │ │ ├── OfferUtilizationAlerts.jsx │ │ ├── PendingAssignmentCancelButton.jsx │ │ ├── PendingAssignmentRemindButton.jsx │ │ ├── RemindAssignmentModal.jsx │ │ ├── SpendTableAmountContents.jsx │ │ ├── SpendTableEnrollmentDetails.jsx │ │ ├── SubBudgetCard.jsx │ │ ├── SubBudgetCardUtilization.jsx │ │ ├── TableTextFilter.jsx │ │ ├── assets │ │ │ ├── phoneScroll.svg │ │ │ ├── reading.svg │ │ │ └── wallet.svg │ │ ├── assignment-modal │ │ │ ├── AssignmentAllocationHelpCollapsibles.jsx │ │ │ ├── AssignmentModalContent.jsx │ │ │ ├── AssignmentModalSummary.jsx │ │ │ ├── AssignmentModalSummaryEmptyState.jsx │ │ │ ├── AssignmentModalSummaryErrorState.jsx │ │ │ ├── AssignmentModalSummaryLearnerList.jsx │ │ │ ├── AssignmentModalmportantDates.jsx │ │ │ ├── CreateAllocationErrorAlertModals.jsx │ │ │ ├── NewAssignmentModalButton.jsx │ │ │ └── NewAssignmentModalDropdown.jsx │ │ ├── assignments-status-chips │ │ │ ├── BaseModalPopup.jsx │ │ │ ├── FailedBadEmail.jsx │ │ │ ├── FailedCancellation.jsx │ │ │ ├── FailedRedemption.jsx │ │ │ ├── FailedReminder.jsx │ │ │ ├── FailedSystem.jsx │ │ │ ├── IncompleteAssignment.jsx │ │ │ ├── NotifyingLearner.jsx │ │ │ └── WaitingForLearner.jsx │ │ ├── cards │ │ │ ├── BaseCourseCard.jsx │ │ │ ├── CourseCard.jsx │ │ │ ├── CourseCardFooterActions.jsx │ │ │ ├── assignment-allocation-status-modals │ │ │ │ ├── ContentNotInCatalogErrorAlertModal.jsx │ │ │ │ ├── NotEnoughBalanceAlertModal.jsx │ │ │ │ └── SystemErrorAlertModal.jsx │ │ │ ├── data │ │ │ │ ├── constants.js │ │ │ │ ├── index.js │ │ │ │ ├── useCourseCardMetadata.jsx │ │ │ │ └── utils.ts │ │ │ └── tests │ │ │ │ └── CourseCard.test.jsx │ │ ├── data │ │ │ ├── constants.js │ │ │ ├── hooks │ │ │ │ ├── index.js │ │ │ │ ├── tests │ │ │ │ │ ├── useBudgetContentAssignments.test.js │ │ │ │ │ ├── useBudgetDetailActivityOverview.test.jsx │ │ │ │ │ ├── useBudgetDetailTabs.test.jsx │ │ │ │ │ ├── useBudgetRedemptions.test.jsx │ │ │ │ │ ├── useCatalogContainsContentItemsMultipleQueries.test.jsx │ │ │ │ │ ├── useEnterpriseCustomer.test.jsx │ │ │ │ │ ├── useEnterpriseFlexGroups.test.jsx │ │ │ │ │ ├── useEnterpriseGroup.test.jsx │ │ │ │ │ ├── useEnterpriseLearners.test.jsx │ │ │ │ │ ├── useEnterpriseOffer.test.jsx │ │ │ │ │ ├── useSubsidyAccessPolicy.test.jsx │ │ │ │ │ └── useSubsidySummaryAnalyticsApi.test.js │ │ │ │ ├── useAllFlexEnterpriseGroups.js │ │ │ │ ├── useBudgetContentAssignments.js │ │ │ │ ├── useBudgetDetailActivityOverview.js │ │ │ │ ├── useBudgetDetailHeaderData.js │ │ │ │ ├── useBudgetDetailTabs.jsx │ │ │ │ ├── useBudgetId.js │ │ │ │ ├── useBudgetRedemptions.js │ │ │ │ ├── useCancelContentAssignments.js │ │ │ │ ├── useCancelContentAssignments.test.jsx │ │ │ │ ├── useCatalogContainsContentItemsMultipleQueries.js │ │ │ │ ├── useContentMetadata.js │ │ │ │ ├── useEnterpriseCustomer.js │ │ │ │ ├── useEnterpriseFlexGroups.js │ │ │ │ ├── useEnterpriseGroup.ts │ │ │ │ ├── useEnterpriseGroupLearners.js │ │ │ │ ├── useEnterpriseGroupMembersTableData.js │ │ │ │ ├── useEnterpriseLearners.js │ │ │ │ ├── useEnterpriseOffer.js │ │ │ │ ├── useEnterpriseRemovedGroupMembers.js │ │ │ │ ├── useGroupDropdownToggle.js │ │ │ │ ├── useIsLargeOrGreater.js │ │ │ │ ├── usePathToCatalogTab.js │ │ │ │ ├── useRemindContentAssignments.js │ │ │ │ ├── useRemindContentAssignments.test.jsx │ │ │ │ ├── useRemoveMember.js │ │ │ │ ├── useStatusChip.jsx │ │ │ │ ├── useSubsidyAccessPolicy.ts │ │ │ │ ├── useSubsidySummaryAnalyticsApi.js │ │ │ │ ├── useSuccessfulAssignmentToastContextValue.js │ │ │ │ ├── useSuccessfulCancellationToastContextValue.js │ │ │ │ ├── useSuccessfulInvitationToastContextValue.js │ │ │ │ ├── useSuccessfulReminderToastContextValue.js │ │ │ │ └── useSuccessfulRemovalToastContextValue.jsx │ │ │ ├── index.js │ │ │ ├── tests │ │ │ │ ├── constants.js │ │ │ │ ├── constants.test.js │ │ │ │ └── utils.test.js │ │ │ ├── types.ts │ │ │ └── utils.js │ │ ├── empty-state │ │ │ ├── NoAssignableBudgetActivity.jsx │ │ │ └── NoBnEBudgetActivity.jsx │ │ ├── index.jsx │ │ ├── invite-modal │ │ │ ├── FileUpload.jsx │ │ │ ├── InviteMembersModalWrapper.jsx │ │ │ ├── InviteModalBudgetCard.jsx │ │ │ ├── InviteModalContent.jsx │ │ │ ├── InviteModalInputFeedback.jsx │ │ │ ├── InviteModalMembershipInfo.jsx │ │ │ ├── InviteModalPermissions.jsx │ │ │ ├── InviteModalSummary.tsx │ │ │ ├── InviteModalSummaryDuplicate.jsx │ │ │ ├── InviteModalSummaryEmptyState.jsx │ │ │ ├── InviteModalSummaryErrorState.jsx │ │ │ ├── InviteModalSummaryLearnerList.jsx │ │ │ ├── InviteSummaryCount.tsx │ │ │ └── tests │ │ │ │ └── InviteMemberModal.test.jsx │ │ ├── members-tab │ │ │ ├── BudgetDetailMembersTabContents.jsx │ │ │ ├── GroupMembersCsvDownloadTableAction.jsx │ │ │ ├── LearnerCreditGroupMembersTable.jsx │ │ │ ├── MemberDetailsTableCell.jsx │ │ │ ├── MemberEnrollmentsTableColumnHeader.jsx │ │ │ ├── MemberStatusTableCell.jsx │ │ │ ├── MemberStatusTableColumnHeader.jsx │ │ │ ├── MembersTableSwitchFilter.jsx │ │ │ ├── bulk-actions │ │ │ │ ├── MemberRemoveAction.jsx │ │ │ │ └── MemberRemoveModal.jsx │ │ │ ├── status-chips │ │ │ │ ├── Accepted.jsx │ │ │ │ ├── BaseStatusChip.jsx │ │ │ │ ├── FailedBadEmail.jsx │ │ │ │ ├── FailedSystem.jsx │ │ │ │ ├── Pending.jsx │ │ │ │ └── Removed.jsx │ │ │ └── tests │ │ │ │ └── MembersTab.test.jsx │ │ ├── requests-tab │ │ │ ├── AmountCell.jsx │ │ │ ├── CustomTableControlBar.jsx │ │ │ ├── RequestDetailsCell.jsx │ │ │ ├── RequestsTable.jsx │ │ │ └── data │ │ │ │ ├── constants.js │ │ │ │ └── hooks │ │ │ │ └── useBnrSubsidyRequests.js │ │ ├── search │ │ │ ├── CatalogSearch.tsx │ │ │ ├── CatalogSearchResults.jsx │ │ │ └── tests │ │ │ │ ├── CatalogSearch.test.tsx │ │ │ │ └── CatalogSearchResults.test.jsx │ │ ├── styles │ │ │ └── index.scss │ │ └── tests │ │ │ ├── BudgetCard.test.jsx │ │ │ ├── BudgetDetailPage.test.jsx │ │ │ ├── BudgetDetailPageWrapper.test.jsx │ │ │ ├── EmailAddressTableCell.test.jsx │ │ │ ├── LearnerCreditAggregateCards.test.jsx │ │ │ ├── LearnerCreditAllocationTable.test.jsx │ │ │ ├── LearnerCreditDisclaimer.test.jsx │ │ │ ├── MultipleBudgetsPage.test.jsx │ │ │ ├── OfferDates.test.jsx │ │ │ ├── OfferUtilizationAlerts.test.jsx │ │ │ └── TableTextFilter.test.jsx │ ├── settings │ │ ├── ConfigErrorModal.test.tsx │ │ ├── ConfigErrorModal.tsx │ │ ├── HelpCenterButton.jsx │ │ ├── SettingsAccessTab │ │ │ ├── ActionsTableCell.jsx │ │ │ ├── DateCreatedTableCell.jsx │ │ │ ├── DisableLinkManagementAlertModal.jsx │ │ │ ├── LinkCopiedToast.jsx │ │ │ ├── LinkDeactivationAlertModal.jsx │ │ │ ├── LinkTableCell.jsx │ │ │ ├── SettingsAccessConfiguredSubsidyType.jsx │ │ │ ├── SettingsAccessGenerateLinkButton.jsx │ │ │ ├── SettingsAccessLinkManagement.jsx │ │ │ ├── SettingsAccessSSOManagement.jsx │ │ │ ├── SettingsAccessSubsidyRequestManagement.jsx │ │ │ ├── SettingsAccessSubsidyTypeSelection.jsx │ │ │ ├── SettingsAccessTabSection.jsx │ │ │ ├── StatusTableCell.jsx │ │ │ ├── UsageTableCell.jsx │ │ │ ├── data │ │ │ │ └── utils.js │ │ │ ├── index.jsx │ │ │ ├── tests │ │ │ │ ├── ActionsTableCell.test.jsx │ │ │ │ ├── DateCreatedTableCell.test.jsx │ │ │ │ ├── DisableLinkManagementAlertModal.test.jsx │ │ │ │ ├── LinkDeactivationAlertModal.test.jsx │ │ │ │ ├── LinkTableCell.test.jsx │ │ │ │ ├── SettingsAccessConfiguredSubsidyType.test.jsx │ │ │ │ ├── SettingsAccessGenerateLinkButton.test.jsx │ │ │ │ ├── SettingsAccessLinkManagement.test.jsx │ │ │ │ ├── SettingsAccessSSOManagement.test.jsx │ │ │ │ ├── SettingsAccessSubsidyRequestManagement.test.jsx │ │ │ │ ├── SettingsAccessSubsidyTypeSelection.test.jsx │ │ │ │ ├── SettingsAccessTab.test.jsx │ │ │ │ ├── SettingsAccessTabSection.test.jsx │ │ │ │ ├── StatusTableCell.test.jsx │ │ │ │ ├── TestUtils.jsx │ │ │ │ ├── UsageTableCell.test.jsx │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── DateCreatedTableCell.test.jsx.snap │ │ │ │ │ ├── LinkTableCell.test.jsx.snap │ │ │ │ │ ├── StatusTableCell.test.jsx.snap │ │ │ │ │ └── UsageTableCell.test.jsx.snap │ │ │ └── utils.js │ │ ├── SettingsApiCredentialsTab │ │ │ ├── APICredentialsPage.jsx │ │ │ ├── Context.jsx │ │ │ ├── CopiedToast.jsx │ │ │ ├── CopyButton.jsx │ │ │ ├── FailedAlert.jsx │ │ │ ├── RegenerateCredentialWarningModal.jsx │ │ │ ├── ZeroStateCard.jsx │ │ │ ├── constants.jsx │ │ │ ├── index.jsx │ │ │ └── tests │ │ │ │ └── SettingsAPICredentialsPage.test.jsx │ │ ├── SettingsAppearanceTab │ │ │ ├── ColorEntryField.tsx │ │ │ ├── CustomThemeModal.tsx │ │ │ ├── ThemeCard.jsx │ │ │ ├── ThemeSvg.jsx │ │ │ ├── index.tsx │ │ │ ├── tests │ │ │ │ └── SettingsAppearanceTab.test.jsx │ │ │ └── types.d.ts │ │ ├── SettingsLMSTab │ │ │ ├── ErrorReporting │ │ │ │ ├── ContentMetadataTable.jsx │ │ │ │ ├── DownloadCsvButton.jsx │ │ │ │ ├── ErrorReportingTable.jsx │ │ │ │ ├── LearnerMetadataTable.jsx │ │ │ │ ├── SyncHistory.jsx │ │ │ │ ├── tests │ │ │ │ │ ├── ErrorReporting.test.jsx │ │ │ │ │ └── SyncHistory.test.jsx │ │ │ │ └── utils.jsx │ │ │ ├── ExistingCard.jsx │ │ │ ├── ExistingLMSCardDeck.jsx │ │ │ ├── LMSConfigPage.jsx │ │ │ ├── LMSConfigs │ │ │ │ ├── Blackboard │ │ │ │ │ ├── BlackboardConfig.tsx │ │ │ │ │ ├── BlackboardConfigAuthorizePage.tsx │ │ │ │ │ └── BlackboardTypes.tsx │ │ │ │ ├── Canvas │ │ │ │ │ ├── CanvasConfig.tsx │ │ │ │ │ ├── CanvasConfigAuthorizePage.tsx │ │ │ │ │ └── CanvasTypes.tsx │ │ │ │ ├── ConfigBasePages │ │ │ │ │ └── ConfigActivatePage.tsx │ │ │ │ ├── Cornerstone │ │ │ │ │ ├── CornerstoneConfig.tsx │ │ │ │ │ ├── CornerstoneConfigEnablePage.tsx │ │ │ │ │ └── CornerstoneTypes.tsx │ │ │ │ ├── Degreed │ │ │ │ │ ├── DegreedConfig.tsx │ │ │ │ │ ├── DegreedConfigEnablePage.tsx │ │ │ │ │ └── DegreedTypes.tsx │ │ │ │ ├── Moodle │ │ │ │ │ ├── MoodleConfig.tsx │ │ │ │ │ ├── MoodleConfigEnablePage.tsx │ │ │ │ │ └── MoodleTypes.tsx │ │ │ │ ├── SAP │ │ │ │ │ ├── SAPConfig.tsx │ │ │ │ │ ├── SAPConfigEnablePage.tsx │ │ │ │ │ └── SAPTypes.tsx │ │ │ │ └── utils.tsx │ │ │ ├── LMSFormWorkflowConfig.tsx │ │ │ ├── LMSSelectorPage.tsx │ │ │ ├── NoConfigCard.jsx │ │ │ ├── UnsavedChangesModal.tsx │ │ │ ├── index.jsx │ │ │ ├── tests │ │ │ │ ├── AuthorizationsConfigs.test.tsx │ │ │ │ ├── BlackboardConfig.test.tsx │ │ │ │ ├── CanvasConfig.test.tsx │ │ │ │ ├── CornerstoneConfig.test.jsx │ │ │ │ ├── DegreedConfig.test.tsx │ │ │ │ ├── ExistingLMSCardDeck.test.jsx │ │ │ │ ├── LmsConfigPage.test.jsx │ │ │ │ ├── MoodleConfig.test.jsx │ │ │ │ └── SAPConfig.test.jsx │ │ │ └── utils.js │ │ ├── SettingsSSOTab │ │ │ ├── ExistingSSOConfigs.jsx │ │ │ ├── NewExistingSSOConfigs.jsx │ │ │ ├── NewSSOConfigAlerts.jsx │ │ │ ├── NewSSOConfigCard.jsx │ │ │ ├── NewSSOConfigForm.jsx │ │ │ ├── NewSSOStepper.jsx │ │ │ ├── NoSSOCard.jsx │ │ │ ├── SSOConfigConfiguredCard.jsx │ │ │ ├── SSOConfigContext.jsx │ │ │ ├── SSOFormWorkflowConfig.tsx │ │ │ ├── SSOStepper.jsx │ │ │ ├── SsoErrorPage.jsx │ │ │ ├── UnsavedSSOChangesModal.tsx │ │ │ ├── data │ │ │ │ ├── actions.js │ │ │ │ ├── reducer.js │ │ │ │ └── reducer.test.js │ │ │ ├── hooks.js │ │ │ ├── index.jsx │ │ │ ├── steps │ │ │ │ ├── NewSSOConfigAuthorizeStep.tsx │ │ │ │ ├── NewSSOConfigConfigureStep.tsx │ │ │ │ ├── NewSSOConfigConfirmStep.tsx │ │ │ │ ├── NewSSOConfigConnectStep.tsx │ │ │ │ ├── SSOConfigConfigureStep.jsx │ │ │ │ ├── SSOConfigConfigureStep.test.jsx │ │ │ │ ├── SSOConfigConnectStep.jsx │ │ │ │ ├── SSOConfigConnectStep.test.jsx │ │ │ │ ├── SSOConfigIDPStep.jsx │ │ │ │ ├── SSOConfigIDPStep.test.jsx │ │ │ │ ├── SSOConfigServiceProviderStep.jsx │ │ │ │ └── SSOConfigServiceProviderStep.test.jsx │ │ │ ├── tests │ │ │ │ ├── ExistingSSOConfigs.test.jsx │ │ │ │ ├── NewExistingSSOConfigs.test.jsx │ │ │ │ ├── NewSSOConfigAlerts.test.jsx │ │ │ │ ├── NewSSOConfigCard.test.jsx │ │ │ │ ├── NewSSOConfigForm.test.jsx │ │ │ │ ├── SSOConfigCard.test.jsx │ │ │ │ ├── SSOConfigPage.test.jsx │ │ │ │ ├── SettingsSSOTab.test.jsx │ │ │ │ └── utils.test.js │ │ │ ├── testutils.js │ │ │ └── utils.js │ │ ├── SettingsTabs.jsx │ │ ├── __mocks__ │ │ │ └── SettingsTabs.jsx │ │ ├── data │ │ │ ├── constants.js │ │ │ ├── hooks.js │ │ │ └── tests │ │ │ │ └── hooks.test.js │ │ ├── index.jsx │ │ ├── settings.scss │ │ ├── tests │ │ │ ├── SettingsPage.test.jsx │ │ │ └── SettingsTabs.test.jsx │ │ └── utils.js │ ├── subscriptions │ │ ├── MultipleSubscriptionPicker.jsx │ │ ├── MultipleSubscriptionsPage.jsx │ │ ├── SubscriptionCard.jsx │ │ ├── SubscriptionData.jsx │ │ ├── SubscriptionDetailContextProvider.jsx │ │ ├── SubscriptionDetailPage.jsx │ │ ├── SubscriptionDetails.jsx │ │ ├── SubscriptionDetailsSkeleton.jsx │ │ ├── SubscriptionManagementPage.jsx │ │ ├── SubscriptionPlanRoutes.jsx │ │ ├── SubscriptionRoutes.jsx │ │ ├── SubscriptionSubsidyRequests.jsx │ │ ├── SubscriptionTabs.jsx │ │ ├── SubscriptionZeroStateMessage.jsx │ │ ├── buttons │ │ │ ├── DownloadCsvButton.jsx │ │ │ ├── InviteLearnersButton.jsx │ │ │ └── __mocks__ │ │ │ │ └── InviteLearnersButton.jsx │ │ ├── data │ │ │ ├── constants.js │ │ │ ├── contextHooks.jsx │ │ │ ├── hooks.js │ │ │ └── utils.js │ │ ├── expiration │ │ │ ├── SubscriptionExpiration.jsx │ │ │ ├── SubscriptionExpirationBanner.jsx │ │ │ ├── SubscriptionExpirationModals.jsx │ │ │ ├── SubscriptionExpiredModal.jsx │ │ │ └── SubscriptionExpiringModal.jsx │ │ ├── index.js │ │ ├── licenses │ │ │ ├── LicenseAllocationDetails.jsx │ │ │ ├── LicenseAllocationHeader.jsx │ │ │ ├── LicenseManagementModals │ │ │ │ ├── LicenseManagementModalHook.js │ │ │ │ ├── LicenseManagementRemindModal.jsx │ │ │ │ ├── LicenseManagementRevokeModal.jsx │ │ │ │ └── tests │ │ │ │ │ ├── LicenseManagementRemindModal.test.jsx │ │ │ │ │ └── LicenseManagementRevokeModal.test.jsx │ │ │ └── LicenseManagementTable │ │ │ │ ├── LicenseManagementTableActionColumn.jsx │ │ │ │ ├── LicenseManagementUserBadge.jsx │ │ │ │ ├── bulk-actions │ │ │ │ ├── EnrollBulkAction.jsx │ │ │ │ ├── EnrollBulkAction.test.jsx │ │ │ │ ├── RemindBulkAction.jsx │ │ │ │ ├── RemindBulkAction.test.jsx │ │ │ │ ├── RevokeBulkAction.jsx │ │ │ │ └── RevokeBulkAction.test.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── tests │ │ │ │ ├── LicenseManagementTableActionColumn.test.jsx │ │ │ │ ├── LicenseManagementUserBadge.test.jsx │ │ │ │ └── index.test.jsx │ │ ├── styles │ │ │ └── index.scss │ │ └── tests │ │ │ ├── MultipleSubscriptionsPage.test.jsx │ │ │ ├── MultipleSubscriptionsPicker.test.jsx │ │ │ ├── SubscriptionCard.test.jsx │ │ │ ├── SubscriptionDetailPage.test.jsx │ │ │ ├── SubscriptionDetails.test.jsx │ │ │ ├── SubscriptionManagementPage.test.jsx │ │ │ ├── SubscriptionRoutes.test.jsx │ │ │ ├── SubscriptionSubsidyRequests.test.jsx │ │ │ ├── SubscriptionTabs.test.jsx │ │ │ ├── SubscriptionZeroStateMessage.test.jsx │ │ │ ├── TestUtilities.jsx │ │ │ ├── data │ │ │ ├── hooks.test.jsx │ │ │ └── utils.test.js │ │ │ ├── expiration │ │ │ ├── SubscriptionExpirationBanner.test.jsx │ │ │ ├── SubscriptionExpirationModals.test.jsx │ │ │ └── SubscriptionExpiredModal.test.jsx │ │ │ └── licenses │ │ │ └── LicenseAllocationHeader.test.jsx │ ├── subsidy-request-management-alerts │ │ ├── NoAvailableCodesBanner.jsx │ │ ├── NoAvailableLicensesBanner.jsx │ │ ├── index.jsx │ │ └── tests │ │ │ ├── NoAvailableCodesBanner.test.jsx │ │ │ └── NoAvailableLicensesBanner.test.jsx │ ├── subsidy-request-modals │ │ ├── ApproveCouponCodeRequestModal.jsx │ │ ├── ApproveLicenseRequestModal.jsx │ │ ├── DeclineSubsidyRequestModal.jsx │ │ ├── data │ │ │ └── hooks.js │ │ ├── index.jsx │ │ └── tests │ │ │ ├── ApproveCouponCodeRequestModal.test.jsx │ │ │ ├── ApproveLicenseRequestModal.test.jsx │ │ │ ├── DeclineSubsidyRequestModal.test.jsx │ │ │ └── hooks.test.jsx │ ├── subsidy-requests │ │ ├── SubsidyRequestsContext.jsx │ │ ├── data │ │ │ ├── hooks.js │ │ │ └── tests │ │ │ │ └── hooks.test.js │ │ ├── index.js │ │ └── tests │ │ │ └── SubsidyRequestsContext.test.jsx │ ├── system-wide-banner │ │ ├── SystemWideWarningBanner.jsx │ │ └── index.js │ └── test │ │ └── testUtils.jsx ├── config │ └── index.js ├── containers │ ├── AdminCards │ │ └── index.jsx │ ├── AdminCardsV2 │ │ └── index.jsx │ ├── AdminPage │ │ ├── AdminPage.test.jsx │ │ └── index.jsx │ ├── AdminPageV2 │ │ ├── AdminPage.test.jsx │ │ └── index.jsx │ ├── CodeAssignmentModal │ │ ├── CodeAssignmentModal.test.jsx │ │ └── index.jsx │ ├── CodeReminderModal │ │ ├── CodeReminderModal.test.jsx │ │ └── index.jsx │ ├── CodeRevokeModal │ │ ├── CodeRevokeModal.test.jsx │ │ └── index.jsx │ ├── CouponDetails │ │ ├── CouponDetails.test.jsx │ │ └── index.jsx │ ├── DownloadCsvButton │ │ ├── DownloadCsvButton.test.jsx │ │ └── index.jsx │ ├── EnterpriseApp │ │ ├── EnterpriseApp.test.jsx │ │ └── index.jsx │ ├── EnterpriseIndexPage │ │ ├── EnterpriseIndexPage.test.jsx │ │ └── index.jsx │ ├── Footer │ │ ├── Footer.test.jsx │ │ ├── __snapshots__ │ │ │ └── Footer.test.jsx.snap │ │ └── index.jsx │ ├── Header │ │ ├── Header.test.jsx │ │ └── index.jsx │ ├── InviteLearnersModal │ │ ├── InviteLearnersModal.test.jsx │ │ └── index.jsx │ ├── SaveTemplateButton │ │ ├── SaveTemplateButton.test.jsx │ │ ├── __snapshots__ │ │ │ └── SaveTemplateButton.test.jsx.snap │ │ └── index.jsx │ ├── Sidebar │ │ ├── Sidebar.test.jsx │ │ ├── __snapshots__ │ │ │ └── Sidebar.test.jsx.snap │ │ └── index.jsx │ ├── SidebarToggle │ │ ├── SidebarToggle.test.jsx │ │ └── index.jsx │ ├── TableContainer │ │ └── index.jsx │ └── TemplateSourceFields │ │ └── index.jsx ├── custom.d.ts ├── data │ ├── actions │ │ ├── codeAssignment.js │ │ ├── codeReminder.js │ │ ├── codeRevoke.js │ │ ├── coupons.js │ │ ├── createPendingEnterpriseUsers.js │ │ ├── csv.js │ │ ├── csv.test.js │ │ ├── dashboardAnalytics.js │ │ ├── dashboardAnalytics.test.js │ │ ├── dashboardInsights.js │ │ ├── emailTemplate.js │ │ ├── emailTemplate.test.js │ │ ├── enterpriseApp.test.ts │ │ ├── enterpriseApp.ts │ │ ├── enterpriseBudgets.js │ │ ├── enterpriseCustomerAdmin.test.ts │ │ ├── enterpriseCustomerAdmin.ts │ │ ├── enterpriseGroups.js │ │ ├── enterpriseGroups.test.js │ │ ├── portalConfiguration.js │ │ ├── portalConfiguration.test.js │ │ ├── sidebar.js │ │ ├── sidebar.test.js │ │ ├── table.js │ │ ├── table.test.js │ │ └── userSubscription.js │ ├── constants │ │ ├── addUsers.js │ │ ├── codeAssignment.js │ │ ├── codeReminder.js │ │ ├── codeRevoke.js │ │ ├── coupons.js │ │ ├── createPendingEntUser.js │ │ ├── csv.js │ │ ├── dashboardAnalytics.js │ │ ├── dashboardInsights.js │ │ ├── emailTemplate.js │ │ ├── enterpriseBudgets.js │ │ ├── enterpriseCustomerAdmin.ts │ │ ├── enterpriseGroups.js │ │ ├── formSubmissions.js │ │ ├── licenseReminder.js │ │ ├── licenseRevoke.js │ │ ├── portalConfiguration.js │ │ ├── sidebar.js │ │ ├── subsidyRequests.js │ │ ├── subsidyTypes.js │ │ ├── table.js │ │ └── userSubscription.js │ ├── hooks.js │ ├── images │ │ ├── NoConfig.svg │ │ ├── NoSSO.svg │ │ ├── SomethingWentWrong.svg │ │ └── ZeroState.svg │ ├── reducers │ │ ├── codeAssignment.js │ │ ├── codeRevoke.js │ │ ├── coupons.js │ │ ├── csv.js │ │ ├── csv.test.js │ │ ├── dashboardAnalytics.js │ │ ├── dashboardAnalytics.test.js │ │ ├── dashboardInsights.js │ │ ├── dashboardInsights.test.js │ │ ├── emailTemplate.js │ │ ├── emailTemplate.test.js │ │ ├── enterpriseBudgets.js │ │ ├── enterpriseBudgets.test.js │ │ ├── enterpriseCustomerAdmin.test.ts │ │ ├── enterpriseCustomerAdmin.ts │ │ ├── enterpriseGroups.js │ │ ├── enterpriseGroups.test.js │ │ ├── index.js │ │ ├── licenseRemind.js │ │ ├── licenseRevoke.js │ │ ├── portalConfiguration.js │ │ ├── portalConfiguration.test.js │ │ ├── sidebar.js │ │ ├── sidebar.test.js │ │ ├── table.js │ │ ├── table.test.js │ │ └── userSubscription.js │ ├── services │ │ ├── DiscoveryApiService.js │ │ ├── EcommerceApiService.js │ │ ├── EnterpriseAccessApiService.ts │ │ ├── EnterpriseCatalogApiService.js │ │ ├── EnterpriseCatalogApiServiceV2.js │ │ ├── EnterpriseDataApiService.js │ │ ├── EnterpriseSubsidyApiService.js │ │ ├── LicenseManagerAPIService.js │ │ ├── LmsApiService.ts │ │ ├── __mocks__ │ │ │ └── codeSearchResultsResponse.json │ │ ├── apiServiceUtils.js │ │ └── tests │ │ │ ├── EnterpriseAccessApiService.test.js │ │ │ ├── EnterpriseCatalogApiService.test.js │ │ │ ├── EnterpriseDataApiService.test.js │ │ │ ├── EnterpriseSubsidyApiService.test.js │ │ │ ├── LmsApiService.test.js │ │ │ └── apiServiceUtils.test.js │ ├── store.js │ └── validation │ │ ├── email.js │ │ └── email.test.js ├── eventTracking.js ├── hoc.jsx ├── hooks │ ├── index.js │ ├── tests │ │ ├── useInterval.test.jsx │ │ └── useOnMount.test.js │ ├── useInterval.jsx │ └── useOnMount.jsx ├── i18n │ └── index.js ├── icons │ ├── Blackboard.svg │ ├── CSOD.png │ ├── Canvas.svg │ ├── Degreed.png │ ├── Moodle.png │ └── SAP.svg ├── index.jsx ├── index.scss ├── jestGlobalSetup.js ├── optimizely.js ├── setupTest.js ├── types.d.ts ├── utils.js └── utils.test.js ├── tsconfig.json ├── webpack.dev-stage.config.js ├── webpack.dev.config.js └── webpack.prod.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | README.md 4 | LICENSE 5 | .babelrc 6 | .eslintignore 7 | .eslintrc.json 8 | .gitignore 9 | .npmignore 10 | commitlint.config.js 11 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV='production' 2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | LMS_BASE_URL='http://localhost:18000' 2 | DATA_API_BASE_URL='http://localhost:8000' 3 | ECOMMERCE_BASE_URL='http://localhost:8000' 4 | LICENSE_MANAGER_BASE_URL='http://localhost:18170' 5 | ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734' 6 | DISCOVERY_BASE_URL='http://localhost:18381' 7 | ENTERPRISE_CATALOG_BASE_URL='http://localhost:18160' 8 | ENTERPRISE_ACCESS_BASE_URL='http://localhost:18270' 9 | ENTERPRISE_SUBSIDY_BASE_URL='http://localhost:18280' 10 | LOGO_URL='https://edx-cdn.org/v3/prod/logo.svg' 11 | LOGO_TRADEMARK_URL='https://edx-cdn.org/v3/default/logo-trademark.svg' 12 | LOGO_WHITE_URL='https://edx-cdn.org/v3/default/logo-white.svg' 13 | FAVICON_URL='https://edx-cdn.org/v3/default/favicon.ico' 14 | FEATURE_FILE_ATTACHMENT='true' 15 | ENTERPRISE_SUPPORT_URL = '' 16 | ENTERPRISE_SUPPORT_REVOKE_LICENSE_URL = '' 17 | PLOTLY_SERVER_URL='http://localhost:8050' 18 | DEMO_ENTEPRISE_UUID='set a valid enterprise uuid' 19 | EDX_ACCESS_URL='https://2u-guid-staging.us.auth0.com' 20 | ANALYTICS_SUPPORTED='true' 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | dist/ 3 | node_modules/ 4 | src/i18n/ 5 | src/segment.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const { getBaseConfig } = require('@openedx/frontend-build'); 3 | 4 | const config = getBaseConfig('eslint'); 5 | /* Custom config manipulations */ 6 | config.rules = { 7 | ...config.rules, 8 | '@typescript-eslint/default-param-last': 'off', 9 | 'react/require-default-props': 'off', 10 | 'import/no-named-as-default': 0, 11 | }; 12 | 13 | config.ignorePatterns = ["*.json", ".eslintrc.js", "*.config.js", "jsdom-with-global.js"]; 14 | 15 | config.overrides = [ 16 | { 17 | files: ['*.test.js', '*.test.jsx', '*.test.ts', '*.test.tsx'], 18 | parser: "@typescript-eslint/parser", 19 | parserOptions: { 20 | project: [ 21 | "./tsconfig.json", 22 | "./functions/tsconfig.json", 23 | ] 24 | } 25 | }, 26 | { 27 | files: ['*.test.js', '*.test.jsx', '*.test.ts', '*.test.tsx'], 28 | rules: { 29 | 'react/prop-types': 'off', 30 | 'react/jsx-no-constructed-context-values': 'off', 31 | }, 32 | }, 33 | ]; 34 | 35 | 36 | 37 | module.exports = config; 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Adding new check for github-actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # For all changes 2 | 3 | - [ ] Ensure adequate tests are in place (or reviewed existing tests cover changes) 4 | 5 | # Only if submitting a visual change 6 | 7 | - [ ] Ensure to attach screenshots 8 | - [ ] Ensure to have UX team confirm screenshots 9 | -------------------------------------------------------------------------------- /.github/workflows/add-depr-ticket-to-depr-board.yml: -------------------------------------------------------------------------------- 1 | # Run the workflow that adds new tickets that are either: 2 | # - labelled "DEPR" 3 | # - title starts with "[DEPR]" 4 | # - body starts with "Proposal Date" (this is the first template field) 5 | # to the org-wide DEPR project board 6 | 7 | name: Add newly created DEPR issues to the DEPR project board 8 | 9 | on: 10 | issues: 11 | types: [opened] 12 | 13 | jobs: 14 | routeissue: 15 | uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master 16 | secrets: 17 | GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }} 18 | GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }} 19 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/add-remove-label-on-comment.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a comment is made on the ticket 2 | # If the comment starts with "label: " it tries to apply 3 | # the label indicated in rest of comment. 4 | # If the comment starts with "remove label: ", it tries 5 | # to remove the indicated label. 6 | # Note: Labels are allowed to have spaces and this script does 7 | # not parse spaces (as often a space is legitimate), so the command 8 | # "label: really long lots of words label" will apply the 9 | # label "really long lots of words label" 10 | 11 | name: Allows for the adding and removing of labels via comment 12 | 13 | on: 14 | issue_comment: 15 | types: [created] 16 | 17 | jobs: 18 | add_remove_labels: 19 | uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | # Run commitlint on the commit messages in a pull request. 2 | 3 | name: Lint Commit Messages 4 | 5 | on: 6 | - pull_request 7 | 8 | jobs: 9 | commitlint: 10 | uses: openedx/.github/.github/workflows/commitlint.yml@master 11 | -------------------------------------------------------------------------------- /.github/workflows/lockfileversion-check.yml: -------------------------------------------------------------------------------- 1 | #check package-lock file version 2 | 3 | name: Lockfile Version check 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | 11 | jobs: 12 | version-check: 13 | uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master 14 | -------------------------------------------------------------------------------- /.github/workflows/self-assign-issue.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a comment is made on the ticket 2 | # If the comment starts with "assign me" it assigns the author to the 3 | # ticket (case insensitive) 4 | 5 | name: Assign comment author to ticket if they say "assign me" 6 | on: 7 | issue_comment: 8 | types: [created] 9 | 10 | jobs: 11 | self_assign_by_comment: 12 | uses: openedx/.github/.github/workflows/self-assign-issue.yml@master 13 | -------------------------------------------------------------------------------- /.github/workflows/update-browserslist-db.yml: -------------------------------------------------------------------------------- 1 | name: Update Browserslist DB 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 1' 5 | workflow_dispatch: 6 | 7 | jobs: 8 | update-browserslist: 9 | uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master 10 | 11 | secrets: 12 | requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | .eslintcache 4 | node_modules 5 | npm-debug.log 6 | coverage 7 | module.config.js* 8 | 9 | dist/ 10 | .idea 11 | 12 | # emacs 13 | *~ 14 | .projectile 15 | 16 | # edx 17 | .env.private 18 | temp/ 19 | src/i18n/transifex_input.json 20 | src/i18n/messages 21 | 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintignore 2 | .eslintrc.json 3 | .gitignore 4 | .travis.yml 5 | docker-compose.yml 6 | Dockerfile 7 | Makefile 8 | npm-debug.log 9 | 10 | config 11 | coverage 12 | node_modules 13 | public 14 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Adam Stankiewicz 2 | Brittney Exline 3 | Christopher Pappas 4 | George Babey 5 | Irfan Ahmad 6 | Mushtaq Ali 7 | Muhammad Ammar 8 | Zubair Afzal 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copied from https://github.com/BretFisher/node-docker-good-defaults/blob/master/Dockerfile 2 | 3 | FROM node:16 4 | 5 | # Create app directory 6 | RUN mkdir -p /edx/app 7 | 8 | ARG NODE_ENV=production 9 | ENV NODE_ENV $NODE_ENV 10 | 11 | ARG PORT=80 12 | ENV PORT $PORT 13 | EXPOSE $PORT 1991 14 | 15 | WORKDIR /edx 16 | # Install app dependencies 17 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 18 | # where available (npm@5+) 19 | COPY package*.json ./ 20 | 21 | # If you are building your code for production 22 | # RUN npm install --only=production 23 | RUN npm install 24 | ENV PATH /edx/app/node_modules/.bin:$PATH 25 | 26 | WORKDIR /edx/app 27 | COPY . /edx/app 28 | 29 | ENTRYPOINT npm install && npm run start 30 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | web: 4 | build: 5 | context: . 6 | args: 7 | - NODE_ENV=development 8 | volumes: 9 | - .:/edx/app:delegated 10 | - notused:/edx/app/node_modules 11 | ports: 12 | - "1991:1991" 13 | environment: 14 | - NODE_ENV=development 15 | 16 | volumes: 17 | notused: 18 | -------------------------------------------------------------------------------- /docs/decisions/0001-record-architecture-decisions.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | 1. Record Architecture Decisions 3 | ================================ 4 | 5 | ****** 6 | Status 7 | ****** 8 | 9 | Accepted 10 | 11 | ******* 12 | Context 13 | ******* 14 | 15 | We would like to keep a historical record on the architectural decisions we make with this app as it evolves over time. 16 | 17 | ******** 18 | Decision 19 | ******** 20 | 21 | We will use Architecture Decision Records, as described by 22 | Michael Nygard in `Documenting Architecture Decisions`_ 23 | 24 | .. _Documenting Architecture Decisions: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions 25 | 26 | ************ 27 | Consequences 28 | ************ 29 | 30 | See Michael Nygard's article, linked above. 31 | 32 | ********** 33 | References 34 | ********** 35 | 36 | * https://resources.sei.cmu.edu/asset_files/Presentation/2017_017_001_497746.pdf 37 | * https://github.com/npryce/adr-tools/tree/master/doc/adr 38 | -------------------------------------------------------------------------------- /docs/how_tos/i18n.rst: -------------------------------------------------------------------------------- 1 | #################### 2 | React App i18n HOWTO 3 | #################### 4 | 5 | This document has moved to the frontend-platform repo: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const { createConfig } = require('@openedx/frontend-build'); 3 | 4 | const config = createConfig('jest', { 5 | setupFiles: [ 6 | '/src/setupTest.js', 7 | ], 8 | }); 9 | config.transformIgnorePatterns = ['node_modules/(?!(lodash-es|@(open)?edx)/)']; 10 | 11 | // This is temoporary changes to exclude the requests-tab folder from coverage 12 | config.coveragePathIgnorePatterns = [ 13 | '/src/components/learner-credit-management/requests-tab/', 14 | 'src/components/learner-credit-management/BudgetDetailRequestsTabContent.jsx', 15 | ]; 16 | 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /jsdom-with-global.js: -------------------------------------------------------------------------------- 1 | const JSDOMEnvironment = require('jest-environment-jsdom'); 2 | 3 | module.exports = class CustomizedJSDomEnvironment extends JSDOMEnvironment { 4 | constructor(config) { 5 | super(config); 6 | this.global.jsdom = this.dom; 7 | } 8 | 9 | teardown() { 10 | this.global.jsdom = null; 11 | return super.teardown(); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /packages/dash-embedded-component-2.0.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/frontend-app-admin-portal/102bd537a81d5582d42206e066e9d36d2a9ab0a0/packages/dash-embedded-component-2.0.2.tgz -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <% if (htmlWebpackPlugin.options.NODE_ENV === 'production' && htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %> 11 | 12 | <% } %> 13 | 14 | <% if (htmlWebpackPlugin.options.NODE_ENV !== 'production' && htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %> 15 | 16 | <% } %> 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended", 4 | "group:allNonMajor" 5 | ], 6 | "patch": { 7 | "automerge": true 8 | }, 9 | "major": { 10 | "dependencyDashboardApproval": true 11 | }, 12 | "rebaseWhen": "behind-base-branch", 13 | "packageRules": [ 14 | { 15 | "matchUpdateTypes": [ 16 | "minor", 17 | "patch" 18 | ], 19 | "automerge": true, 20 | "matchPackageNames": [ 21 | "/@edx/", 22 | "/@openedx/" 23 | ] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/colors.scss: -------------------------------------------------------------------------------- 1 | // Only permanent colors should be defined here that stay the same 2 | // no matter which paragon or enterprise themes you apply. 3 | $product-tours-background-color: #D23228; 4 | -------------------------------------------------------------------------------- /src/components/Admin/AIAnalyticsSummarySkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton, Stack } from '@openedx/paragon'; 3 | 4 | const AIAnalyticsSummarySkeleton = () => ( 5 | 6 | 7 | {/* Placeholder for Track Progress is currently hidden due to data inconsistency, will be addressed in ENT-7812 */} 8 | 9 | 10 | ); 11 | 12 | export default AIAnalyticsSummarySkeleton; 13 | -------------------------------------------------------------------------------- /src/components/Admin/AdminCardsSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from '@openedx/paragon'; 3 | import { FormattedMessage } from '@edx/frontend-platform/i18n'; 4 | 5 | const AdminCardsSkeleton = () => ( 6 |
9 |
10 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | ); 22 | 23 | export default AdminCardsSkeleton; 24 | -------------------------------------------------------------------------------- /src/components/Admin/AdminCardsSkeleton.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { IntlProvider } from '@edx/frontend-platform/i18n'; 4 | import AdminCardsSkeleton from './AdminCardsSkeleton'; 5 | 6 | describe('AdminCardsSkeleton', () => { 7 | it('renders a skeleton', () => { 8 | const tree = renderer 9 | .create(( 10 | 11 | 12 | 13 | )) 14 | .toJSON(); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/Admin/_Admin.scss: -------------------------------------------------------------------------------- 1 | .learner-progress-report { 2 | .table-title { 3 | display: inline-block; 4 | vertical-align: middle; 5 | } 6 | 7 | .reset { 8 | margin-top: -5px; 9 | } 10 | 11 | .search-label { 12 | line-height: 1.5; 13 | } 14 | 15 | .admin-cards-skeleton { 16 | span { 17 | min-height: 200px; 18 | width: 100%; 19 | .react-loading-skeleton { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | } 24 | } 25 | 26 | @include media-breakpoint-up(sm) { 27 | .admin-cards-skeleton { 28 | span { 29 | width: 23%; 30 | margin-right: 2%; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Admin/licenses/LicenseAllocationDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LicenseAllocationHeader from './LicenseAllocationHeader'; 4 | import LicenseManagementTable from './LicenseManagementTable'; 5 | 6 | const LicenseAllocationDetails = ({ subscriptionUUID }) => ( 7 |
8 |
9 |
10 | 11 |
12 | 13 |
14 |
15 | ); 16 | 17 | LicenseAllocationDetails.propTypes = { 18 | subscriptionUUID: PropTypes.string.isRequired, 19 | }; 20 | export default LicenseAllocationDetails; 21 | -------------------------------------------------------------------------------- /src/components/Admin/licenses/__snapshots__/LicenseAllocationHeader.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LicenseAllocationHeader renders without crashing 1`] = ` 4 |
7 |

14 | Licenses: 15 |

16 | 19 | Unassigned: 1 of 2 total 20 | 21 | 24 | Activated: 1 of 2 assigned 25 | 26 |
27 | `; 28 | -------------------------------------------------------------------------------- /src/components/AdminV2/AIAnalyticsSummarySkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton, Stack } from '@openedx/paragon'; 3 | 4 | const AIAnalyticsSummarySkeleton = () => ( 5 | 6 | 7 | 8 | {/* Placeholder for Track Progress is currently hidden due to data inconsistency, 9 | will be addressed in ENT-7812 10 | */} 11 | 12 | 13 | 14 | ); 15 | 16 | export default AIAnalyticsSummarySkeleton; 17 | -------------------------------------------------------------------------------- /src/components/AdminV2/AdminCardsSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from '@openedx/paragon'; 3 | 4 | const AdminCardsSkeleton = () => ( 5 |
8 |
Loading...
9 | 10 | 11 | 12 | 13 |
14 | ); 15 | 16 | export default AdminCardsSkeleton; 17 | -------------------------------------------------------------------------------- /src/components/AdminV2/_Admin.scss: -------------------------------------------------------------------------------- 1 | .learner-progress-report { 2 | .table-title { 3 | display: inline-block; 4 | vertical-align: middle; 5 | } 6 | 7 | .search-label { 8 | line-height: 1.5; 9 | } 10 | 11 | .admin-cards-skeleton { 12 | span { 13 | min-height: 200px; 14 | width: 100%; 15 | .react-loading-skeleton { 16 | width: 100%; 17 | height: 100%; 18 | } 19 | } 20 | } 21 | 22 | @include media-breakpoint-up(sm) { 23 | .admin-cards-skeleton { 24 | span { 25 | width: 23%; 26 | margin-right: 2%; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/AdminV2/cards/NumberCard/DetailsAction.test.jsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter } from 'react-router-dom'; 2 | import { IntlProvider } from '@edx/frontend-platform/i18n'; 3 | import { screen, render } from '@testing-library/react'; 4 | import '@testing-library/jest-dom/extend-expect'; 5 | import DetailsAction from './DetailsAction'; 6 | 7 | jest.mock('react-router-dom', () => ({ 8 | ...jest.requireActual('react-router-dom'), 9 | useLocation: jest.fn().mockReturnValue({ pathname: '/admin' }), 10 | })); 11 | 12 | const DetailsActionWrapper = () => ( 13 | 14 | 15 | {}} 20 | /> 21 | 22 | 23 | ); 24 | 25 | describe(' ', () => { 26 | it('renders correctly', () => { 27 | render( 28 | , 29 | ); 30 | expect(screen.getByText('Action Label')).toBeInTheDocument(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/AdminV2/tests/AdminCardsSkeleton.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import AdminCardsSkeleton from '../AdminCardsSkeleton'; 5 | 6 | describe('AdminCardsSkeleton', () => { 7 | it('renders a skeleton', () => { 8 | const { container } = render(); 9 | 10 | const skeletonContainer = container.querySelector('.admin-cards-skeleton'); 11 | expect(skeletonContainer).toBeInTheDocument(); 12 | 13 | expect(skeletonContainer).toHaveTextContent('Loading...'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/AdvanceAnalyticsV2/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Header = ({ 5 | title, subtitle, DownloadCSVComponent, 6 | }) => ( 7 |
8 |
9 |

{title}

10 | {subtitle &&

{subtitle}

} 11 |
12 |
13 | {DownloadCSVComponent} 14 |
15 |
16 | ); 17 | 18 | Header.defaultProps = { 19 | subtitle: undefined, 20 | }; 21 | 22 | Header.propTypes = { 23 | title: PropTypes.string.isRequired, 24 | subtitle: PropTypes.string, 25 | DownloadCSVComponent: PropTypes.element.isRequired, 26 | }; 27 | 28 | export default Header; 29 | -------------------------------------------------------------------------------- /src/components/AdvanceAnalyticsV2/ProgressOverlay.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Spinner, 3 | } from '@openedx/paragon'; 4 | import PropTypes from 'prop-types'; 5 | import EmptyChart from './charts/EmptyChart'; 6 | 7 | const ProgressOverlay = ({ isError, message }) => ( 8 |
9 |
10 | {isError ? : } 11 |
12 |
13 | ); 14 | 15 | ProgressOverlay.propTypes = { 16 | isError: PropTypes.bool.isRequired, 17 | message: PropTypes.string, 18 | }; 19 | 20 | export default ProgressOverlay; 21 | -------------------------------------------------------------------------------- /src/components/AdvanceAnalyticsV2/constants.js: -------------------------------------------------------------------------------- 1 | // Default group for the analytics. For now, it will be an empty string. 2 | export const DEFAULT_GROUP = ''; 3 | -------------------------------------------------------------------------------- /src/components/AdvanceAnalyticsV2/styles/index.scss: -------------------------------------------------------------------------------- 1 | .analytics-stat-number { 2 | font-size: 2.5rem; 3 | } 4 | 5 | @mixin fetching-overlay { 6 | content: ""; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | height: 100%; 12 | background-color: rgba($white, 0.7); 13 | z-index: 1; 14 | } 15 | 16 | @mixin spinner-centered { 17 | position: absolute; 18 | top: 50%; 19 | left: 50%; 20 | transform: translate(-50%, -50%); 21 | z-index: 2; 22 | } 23 | 24 | .analytics-chart-container, 25 | .stats-container { 26 | position: relative; 27 | 28 | &.is-fetching::before { 29 | @include fetching-overlay; 30 | } 31 | 32 | .spinner-centered { 33 | @include spinner-centered; 34 | } 35 | } 36 | .analytics-chart-container{ 37 | min-height: 40vh; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/components/AdvanceAnalyticsV2/tests/Header.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import Header from '../Header'; 4 | 5 | describe('Header', () => { 6 | it('renders correctly with both title and subtitle', () => { 7 | const wrapper = shallow(
); 8 | expect(wrapper.find('.analytics-header-title').text()).toBe('Test Title'); 9 | expect(wrapper.find('.analytics-header-subtitle').text()).toBe('Test Subtitle'); 10 | }); 11 | 12 | it('renders correctly with only the title', () => { 13 | const wrapper = shallow(
); 14 | expect(wrapper.find('.analytics-header-title').text()).toBe('Test Title'); 15 | expect(wrapper.find('.analytics-header-subtitle').exists()).toBeFalsy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/AuthenticatedEnterpriseApp/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LoginRedirect } from '@edx/frontend-enterprise-logistration'; 3 | 4 | import EnterpriseApp from '../../containers/EnterpriseApp'; 5 | import EnterpriseAppSkeleton from '../EnterpriseApp/EnterpriseAppSkeleton'; 6 | 7 | const AuthenticatedEnterpriseApp = () => ( 8 | } 10 | > 11 | 12 | 13 | ); 14 | 15 | export default AuthenticatedEnterpriseApp; 16 | -------------------------------------------------------------------------------- /src/components/BrandStyles/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import { useStylesForCustomBrandColors } from '../settings/data/hooks'; 6 | 7 | const BrandStyles = ({ 8 | enterpriseBranding, 9 | }) => { 10 | const brandStyles = useStylesForCustomBrandColors(enterpriseBranding); 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | BrandStyles.defaultProps = { 20 | enterpriseBranding: null, 21 | }; 22 | 23 | BrandStyles.propTypes = { 24 | enterpriseBranding: PropTypes.shape({ 25 | enterpriseId: PropTypes.string, 26 | logo: PropTypes.string, 27 | primary_color: PropTypes.string, 28 | secondary_color: PropTypes.string, 29 | tertiary_color: PropTypes.string, 30 | }), 31 | }; 32 | 33 | export default BrandStyles; 34 | -------------------------------------------------------------------------------- /src/components/BudgetExpiryAlertAndModal/data/constants.js: -------------------------------------------------------------------------------- 1 | export const SEEN_ENTERPRISE_EXPIRATION_ALERT_COOKIE_PREFIX = 'seen-enterprise-expiration-alert-'; 2 | 3 | export const SEEN_ENTERPRISE_EXPIRATION_MODAL_COOKIE_PREFIX = 'seen-enterprise-expiration-modal-'; 4 | 5 | export const PLAN_EXPIRY_VARIANTS = { 6 | expired: 'Expired', 7 | expiring: 'Expiring', 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/BulkEnrollDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import BulkEnrollmentStepper from './stepper/BulkEnrollmentStepper'; 6 | import BulkEnrollContextProvider from './BulkEnrollmentContext'; 7 | 8 | /** 9 | * @param {object} props Props 10 | * @param {array} props.learners learner email list to enroll 11 | */ 12 | const BulkEnrollDialog = (props) => { 13 | const { learners } = props; 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | const mapStateToProps = state => ({ 22 | enterpriseSlug: state.portalConfiguration.enterpriseSlug, 23 | enterpriseId: state.portalConfiguration.enterpriseId, 24 | }); 25 | 26 | BulkEnrollDialog.propTypes = { 27 | learners: PropTypes.arrayOf(PropTypes.string).isRequired, 28 | }; 29 | 30 | export default connect(mapStateToProps)(BulkEnrollDialog); 31 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/data/actions.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | setSelectedRowsAction, 3 | deleteSelectedRowAction, 4 | clearSelectionAction, 5 | } from './actions'; 6 | import { 7 | SET_SELECTED_ROWS, 8 | DELETE_ROW, 9 | CLEAR_SELECTION, 10 | } from './constants'; 11 | 12 | describe('selectedRows actions', () => { 13 | it('setSelectedRows returns an action with rows and the correct type', () => { 14 | const rows = [{ id: 1 }]; 15 | const result = setSelectedRowsAction(rows); 16 | expect(result).toEqual({ type: SET_SELECTED_ROWS, payload: rows }); 17 | }); 18 | it('deleteSelectedRow returns an action with an id and the correct type', () => { 19 | const result = deleteSelectedRowAction(2); 20 | expect(result).toEqual({ 21 | type: DELETE_ROW, 22 | payload: 2, 23 | }); 24 | }); 25 | it('clearSelection returns and action with the correct type', () => { 26 | const result = clearSelectionAction(); 27 | expect(result).toEqual({ 28 | type: CLEAR_SELECTION, 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/data/actions.ts: -------------------------------------------------------------------------------- 1 | import { CLEAR_SELECTION, DELETE_ROW, SET_SELECTED_ROWS } from './constants'; 2 | import { Action } from './types'; 3 | 4 | export const setSelectedRowsAction = (selectedRowIds) => ({ 5 | type: SET_SELECTED_ROWS, 6 | payload: selectedRowIds, 7 | } as Action); 8 | 9 | export const deleteSelectedRowAction = (rowId) => ({ 10 | type: DELETE_ROW, 11 | payload: rowId, 12 | } as Action); 13 | 14 | export const clearSelectionAction = () => ({ 15 | type: CLEAR_SELECTION, 16 | } as Action); 17 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/data/constants.ts: -------------------------------------------------------------------------------- 1 | export const SET_SELECTED_ROWS = 'SET SELECTED ROWS'; 2 | export const DELETE_ROW = 'DELETE ROW'; 3 | export const CLEAR_SELECTION = 'CLEAR SELECTION'; 4 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/data/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SELECTED_ROWS, 3 | DELETE_ROW, 4 | CLEAR_SELECTION, 5 | } from './constants'; 6 | import { Action, State } from './types'; 7 | 8 | const selectedRowsReducer = (state: State = [], action: Action) => { 9 | switch (action.type) { 10 | case SET_SELECTED_ROWS: 11 | return action.payload; 12 | case DELETE_ROW: 13 | return state.filter((row) => row.id !== action.payload); 14 | case CLEAR_SELECTION: 15 | return []; 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default selectedRowsReducer; 22 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/data/types.ts: -------------------------------------------------------------------------------- 1 | import { CLEAR_SELECTION, DELETE_ROW, SET_SELECTED_ROWS } from './constants'; 2 | 3 | export type SelectedRow = { 4 | id: unknown; 5 | values?: { 6 | userEmail?: string; 7 | }; 8 | }; 9 | 10 | export type State = SelectedRow[]; 11 | 12 | export type Action = 13 | | { type: typeof SET_SELECTED_ROWS, payload: SelectedRow[] } 14 | | { type: typeof DELETE_ROW, payload: unknown } 15 | | { type: typeof CLEAR_SELECTION }; 16 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/stepper/DismissibleCourseWarning.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Alert } from '@openedx/paragon'; 5 | import { WarningFilled } from '@openedx/paragon/icons'; 6 | import { WARNING_ALERT_TITLE_TEXT, WARNING_ALERT_BODY_TEXT } from './constants'; 7 | 8 | /** 9 | * Displays simple dismissible banner to remind admins. 10 | */ 11 | const DismissibleCourseWarning = ({ defaultShow = false }) => { 12 | const [isOpen, setIsOpen] = useState(defaultShow); 13 | return ( 14 | setIsOpen(false)} 20 | > 21 | {WARNING_ALERT_TITLE_TEXT} 22 |

23 | {WARNING_ALERT_BODY_TEXT} 24 |

25 |
26 | ); 27 | }; 28 | 29 | DismissibleCourseWarning.propTypes = { 30 | defaultShow: PropTypes.bool, 31 | }; 32 | 33 | export default DismissibleCourseWarning; 34 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/table/BulkEnrollSelect.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { CheckboxControl } from '@openedx/paragon'; 5 | 6 | export const SELECT_ONE_TEST_ID = 'selectOne'; 7 | export const SELECT_ALL_TEST_ID = 'selectAll'; 8 | 9 | export const BaseSelectWithContext = ({ row }) => { 10 | const { 11 | indeterminate, 12 | checked, 13 | ...toggleRowSelectedProps 14 | } = row.getToggleRowSelectedProps(); 15 | 16 | return ( 17 |
18 | 25 |
26 | ); 27 | }; 28 | 29 | BaseSelectWithContext.propTypes = { 30 | row: PropTypes.shape({ 31 | getToggleRowSelectedProps: PropTypes.func.isRequired, 32 | }).isRequired, 33 | /* The key to get the required data from BulkEnrollContext */ 34 | contextKey: PropTypes.string.isRequired, 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/BulkEnrollmentPage/table/CourseSearchResultsCells.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { CourseNameCell, FormattedDateCell } from './CourseSearchResultsCells'; 4 | 5 | const testCourseName = 'TestCourseName'; 6 | const testCourseRunKey = 'TestCourseRun'; 7 | const testStartDate = '2020-09-10T10:00:00Z'; 8 | const testEndDate = '2030-09-10T10:00:00Z'; 9 | 10 | describe('CourseNameCell', () => { 11 | const row = { 12 | original: { 13 | key: testCourseRunKey, 14 | }, 15 | }; 16 | const slug = 'sluggy'; 17 | const wrapper = mount(); 18 | it('displays the course name', () => { 19 | expect(wrapper.text()).toEqual(testCourseName); 20 | }); 21 | }); 22 | 23 | describe('', () => { 24 | it('renders a formatted date', () => { 25 | const wrapper = mount(); 26 | expect(wrapper.text()).toEqual('Sep 10, 2020 - Sep 10, 2030'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/CodeAssignmentModal/CodeAssignmentModal.scss: -------------------------------------------------------------------------------- 1 | .modal-dialog.code-assignment { 2 | .modal-title { 3 | span { 4 | font-weight: 600; 5 | } 6 | } 7 | 8 | .modal-body { 9 | .field-group { 10 | p { 11 | font-weight: 600; 12 | } 13 | } 14 | 15 | p { 16 | margin-bottom: 6px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/CodeAssignmentModal/emailTemplate.js: -------------------------------------------------------------------------------- 1 | const emailTemplate = { 2 | subject: 'New edX course assignment', 3 | greeting: `Your Learning Manager has provided you with an access code to take a course at edX. 4 | `, 5 | body: `You may redeem this code for {REDEMPTIONS_REMAINING} course(s). 6 | 7 | edX Login: {USER_EMAIL} 8 | Access Code: {CODE} 9 | Code Expiration Date: {EXPIRATION_DATE} 10 | `, 11 | closing: ` 12 | If your organization uses a branded learner portal, this code will be automatically applied to your account and can be redeemed for any available course. 13 | If your organization does not have a branded learner portal, you can insert the access code at checkout under "coupon code" for applicable courses. 14 | 15 | For any questions, please reach out to your Learning Manager.`, 16 | files: [], 17 | }; 18 | 19 | export default emailTemplate; 20 | -------------------------------------------------------------------------------- /src/components/CodeAssignmentModal/messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from '@edx/frontend-platform/i18n'; 2 | 3 | const messages = defineMessages({ 4 | modalAltText: { 5 | id: 'adminPortal.assignmentModal.altText', 6 | defaultMessage: 'More information', 7 | }, 8 | modalTooltipText: { 9 | id: 'adminPortal.assignmentModal.tooltipText', 10 | defaultMessage: 'edX will remind learners to redeem their code 3, 10, and 19 days after you assign it.', 11 | }, 12 | modalFieldLabel: { 13 | id: 'adminPortal.assignmentModal.modalFieldLabel', 14 | defaultMessage: 'Automate reminders', 15 | }, 16 | }); 17 | 18 | export default messages; 19 | -------------------------------------------------------------------------------- /src/components/CodeManagement/CodeManagementRoutes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | Routes, 6 | Route, 7 | Navigate, 8 | } from 'react-router-dom'; 9 | 10 | import NotFoundPage from '../NotFoundPage'; 11 | import CouponCodeTabs from './CouponCodeTabs'; 12 | import { 13 | DEFAULT_TAB, 14 | COUPON_CODE_TAB_PARAM, 15 | } from './data/constants'; 16 | 17 | const CodeManagementRoutes = ({ enterpriseSlug }) => ( 18 | 19 | } 22 | /> 23 | } 26 | /> 27 | } /> 28 | 29 | ); 30 | 31 | CodeManagementRoutes.propTypes = { 32 | enterpriseSlug: PropTypes.string.isRequired, 33 | }; 34 | 35 | const mapStateToProps = state => ({ 36 | enterpriseSlug: state.portalConfiguration.enterpriseSlug, 37 | }); 38 | 39 | export default connect(mapStateToProps)(CodeManagementRoutes); 40 | -------------------------------------------------------------------------------- /src/components/CodeManagement/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import { Container } from '@openedx/paragon'; 4 | 5 | import Hero from '../Hero'; 6 | import CodeManagementRoutes from './CodeManagementRoutes'; 7 | 8 | const CodeManagement = () => ( 9 | <> 10 | 11 | Code Management 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 | ); 21 | 22 | export default CodeManagement; 23 | -------------------------------------------------------------------------------- /src/components/CodeModal/ModalError.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Alert, Icon } from '@openedx/paragon'; 4 | import { Warning } from '@openedx/paragon/icons'; 5 | 6 | const ModalError = React.forwardRef(({ title, errors }, ref) => ( 7 |
11 | 12 | 13 | {title && ( 14 | {title} 15 | )} 16 | {errors.length > 1 ? ( 17 |
    18 | {errors.map(message =>
  • {message}
  • )} 19 |
20 | ) : ( 21 | errors[0] 22 | )} 23 |
24 |
25 | )); 26 | 27 | ModalError.propTypes = { 28 | title: PropTypes.string.isRequired, 29 | errors: PropTypes.arrayOf(PropTypes.string).isRequired, 30 | }; 31 | 32 | export default ModalError; 33 | -------------------------------------------------------------------------------- /src/components/CodeModal/ModalError.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { screen, render } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import { ModalError } from '.'; 5 | 6 | const props = { 7 | title: 'So many errors!!', 8 | errors: ['wrong', 'bad', 'no', 'just do not'], 9 | }; 10 | 11 | describe('ModalError component', () => { 12 | it('displays a title', () => { 13 | render(); 14 | expect(screen.getByText(props.title)).toBeInTheDocument(); 15 | }); 16 | it('displays errors', () => { 17 | render(); 18 | props.errors.forEach((err) => { 19 | expect(screen.getByText(err)).toBeInTheDocument(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/CodeModal/codeModalHelpers.js: -------------------------------------------------------------------------------- 1 | export const displayCode = (code) => `Code: ${code}`; 2 | export const displayEmail = (email) => `Email: ${email}`; 3 | export const displaySelectedCodes = (numSelectedCodes) => `Selected codes: ${numSelectedCodes}`; 4 | 5 | export function appendUserCodeDetails(assignedEmail, assignedCode, assignments) { 6 | assignments.push({ 7 | user: { 8 | email: assignedEmail, 9 | }, 10 | code: assignedCode, 11 | }); 12 | return assignments; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/CodeModal/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as ModalError } from './ModalError'; 2 | export { 3 | appendUserCodeDetails, displayCode, displayEmail, displaySelectedCodes, 4 | } from './codeModalHelpers'; 5 | -------------------------------------------------------------------------------- /src/components/CodeReminderModal/CodeReminderModal.scss: -------------------------------------------------------------------------------- 1 | .modal-dialog.code-reminder { 2 | .modal-title { 3 | span { 4 | font-weight: 600; 5 | } 6 | } 7 | 8 | .modal-body { 9 | .field-group { 10 | p { 11 | font-weight: 600; 12 | } 13 | } 14 | 15 | p { 16 | margin-bottom: 6px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/CodeReminderModal/emailTemplate.js: -------------------------------------------------------------------------------- 1 | const emailTemplate = { 2 | subject: 'Reminder on edX course assignment', 3 | greeting: `This is a reminder that your Learning Manager has provided you with an access code to take a course at edX. 4 | `, 5 | body: `You have redeemed this code {REDEEMED_OFFER_COUNT} time(s) out of {TOTAL_OFFER_COUNT} available course redemptions. 6 | 7 | edX Login: {USER_EMAIL} 8 | Access Code: {CODE} 9 | Code Expiration Date: {EXPIRATION_DATE} 10 | `, 11 | closing: `If your organization uses a branded learner portal, this code will be automatically applied to your account and can be redeemed for any available course. 12 | If your organization does not have a branded learner portal, you can insert the access code at checkout under "coupon code" for applicable courses. 13 | 14 | For any questions, please reach out to your Learning Manager.`, 15 | files: [], 16 | }; 17 | 18 | export default emailTemplate; 19 | -------------------------------------------------------------------------------- /src/components/CodeRevokeModal/emailTemplate.js: -------------------------------------------------------------------------------- 1 | const emailTemplate = { 2 | subject: 'edX Course Assignment Revoked', 3 | greeting: '', 4 | body: 'Your Learning Manager has revoked access code {CODE} and it is no longer assigned to your edX account {USER_EMAIL}.', 5 | closing: ` 6 | For any questions, please reach out to your Learning Manager.`, 7 | files: [], 8 | }; 9 | 10 | export default emailTemplate; 11 | -------------------------------------------------------------------------------- /src/components/CodeSearchResults/CodeSearchResultsHeading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button, Icon } from '@openedx/paragon'; 4 | import { Close } from '@openedx/paragon/icons'; 5 | 6 | const CodeSearchResultsHeading = ({ searchQuery, onClose }) => ( 7 |
8 |
9 |

10 | Search results for "{searchQuery}" 11 |

12 |
13 |
14 | 22 |
23 |
24 | ); 25 | 26 | CodeSearchResultsHeading.propTypes = { 27 | searchQuery: PropTypes.string.isRequired, 28 | onClose: PropTypes.func.isRequired, 29 | }; 30 | 31 | export default CodeSearchResultsHeading; 32 | -------------------------------------------------------------------------------- /src/components/CodeSearchResults/_CodeSearchResults.scss: -------------------------------------------------------------------------------- 1 | .code-search-results { 2 | .loading { 3 | @extend .mb-3; 4 | } 5 | .table { 6 | @extend .bg-white; 7 | td { 8 | vertical-align: middle; 9 | } 10 | } 11 | .pagination { 12 | @extend .mb-0; 13 | button { 14 | font-size: $font-size-sm; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CompletedLearnersTable renders empty state correctly 1`] = ` 4 |
8 | 11 | 21 | 25 | 26 | 27 |
30 |
33 | There are no results. 34 |
35 |
36 |
37 | `; 38 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/CatalogVisibility/ContentHighlightCatalogVisibility.jsx: -------------------------------------------------------------------------------- 1 | import ContentHighlightCatalogVisibilityAlert from './ContentHighlightCatalogVisibilityAlert'; 2 | import ContentHighlightCatalogVisibilityHeader from './ContentHighlightCatalogVisibilityHeader'; 3 | import ContentHighlightCatalogVisibilityRadioInput from './ContentHighlightCatalogVisibilityRadioInput'; 4 | 5 | const ContentHighlightCatalogVisibility = () => ( 6 | <> 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default ContentHighlightCatalogVisibility; 14 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/CatalogVisibility/index.js: -------------------------------------------------------------------------------- 1 | import ContentHighlightCatalogVisibility from './ContentHighlightCatalogVisibility'; 2 | 3 | export default ContentHighlightCatalogVisibility; 4 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/ContentHighlightHelmet.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import Proptypes from 'prop-types'; 4 | 5 | const ContentHighlightHelmet = ({ title }) => ( 6 | 7 | {title} 8 | 9 | ); 10 | 11 | ContentHighlightHelmet.propTypes = { 12 | title: Proptypes.string.isRequired, 13 | }; 14 | 15 | export default ContentHighlightHelmet; 16 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/ContentHighlightSet.jsx: -------------------------------------------------------------------------------- 1 | import { Container } from '@openedx/paragon'; 2 | import { useParams } from 'react-router-dom'; 3 | import React from 'react'; 4 | import ContentHighlightsCardItemContainer from './ContentHighlightsCardItemsContainer'; 5 | import CurrentContentHighlightItemsHeader from './CurrentContentHighlightItemsHeader'; 6 | import { useHighlightSet } from './data/hooks'; 7 | 8 | const ContentHighlightSet = () => { 9 | const { highlightSetUUID } = useParams(); 10 | const { highlightSet, isLoading, updateHighlightSet } = useHighlightSet(highlightSetUUID); 11 | return ( 12 | 13 | 14 | 19 | 20 | ); 21 | }; 22 | 23 | export default ContentHighlightSet; 24 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/ContentHighlightToast.jsx: -------------------------------------------------------------------------------- 1 | import { Toast } from '@openedx/paragon'; 2 | import React, { useState, useEffect } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const ContentHighlightToast = ({ toastText }) => { 6 | /* Toast visibility state initially set to false to ensure the toast's 7 | fade-in animation occurs on mount once `showToast` set to true. */ 8 | const [showToast, setShowToast] = useState(false); 9 | const handleClose = () => { 10 | setShowToast(false); 11 | }; 12 | useEffect(() => { 13 | setShowToast(true); 14 | }, []); 15 | return ( 16 | handleClose()} 18 | show={showToast} 19 | > 20 | {toastText} 21 | 22 | 23 | ); 24 | }; 25 | 26 | ContentHighlightToast.propTypes = { 27 | toastText: PropTypes.string.isRequired, 28 | }; 29 | 30 | export default ContentHighlightToast; 31 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/CurrentContentHighlights.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Stack, 4 | } from '@openedx/paragon'; 5 | 6 | import ContentHighlightCardContainer from './ContentHighlightCardContainer'; 7 | import CurrentContentHighlightHeader from './CurrentContentHighlightHeader'; 8 | 9 | const CurrentContentHighlights = () => ( 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | export default CurrentContentHighlights; 17 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContent.jsx: -------------------------------------------------------------------------------- 1 | import { Row, Col, Container } from '@openedx/paragon'; 2 | import HighlightStepperSelectContentSearch from './HighlightStepperSelectContentSearch'; 3 | import HighlightStepperSelectContentHeader from './HighlightStepperSelectContentHeader'; 4 | 5 | const HighlightStepperSelectContent = () => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default HighlightStepperSelectContent; 21 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useContextSelector } from 'use-context-selector'; 3 | import { Icon } from '@openedx/paragon'; 4 | import { AddCircle } from '@openedx/paragon/icons'; 5 | import { STEPPER_STEP_TEXT } from '../data/constants'; 6 | import { ContentHighlightsContext } from '../ContentHighlightsContext'; 7 | 8 | const HighlightStepperSelectContentTitle = () => { 9 | const highlightTitle = useContextSelector(ContentHighlightsContext, v => v[0].stepperModal.highlightTitle); 10 | return ( 11 | <> 12 |

13 | 14 | {STEPPER_STEP_TEXT.HEADER_TEXT.selectContent} 15 |

16 |

17 | {STEPPER_STEP_TEXT.SUB_TEXT.selectContent(highlightTitle)} 18 |

19 |

20 | 21 | {STEPPER_STEP_TEXT.PRO_TIP_TEXT.selectContent} 22 | 23 |

24 | 25 | ); 26 | }; 27 | export default HighlightStepperSelectContentTitle; 28 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/SkeletonContentCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '@openedx/paragon'; 3 | 4 | const SkeletonContentCard = () => ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default SkeletonContentCard; 13 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/SkeletonContentCardContainer.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { CardGrid } from '@openedx/paragon'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import SkeletonContentCard from './SkeletonContentCard'; 5 | import { HIGHLIGHTS_CARD_GRID_COLUMN_SIZES } from './data/constants'; 6 | 7 | const SkeletonContentCardContainer = ({ itemCount, columnSizes }) => ( 8 | 9 | {[ 10 | ...new Array(itemCount), 11 | ].map(() => )}; 12 | 13 | ); 14 | 15 | SkeletonContentCardContainer.propTypes = { 16 | itemCount: PropTypes.number.isRequired, 17 | columnSizes: PropTypes.shape({ 18 | xs: PropTypes.number, 19 | md: PropTypes.number, 20 | lg: PropTypes.number, 21 | xl: PropTypes.number, 22 | }), 23 | }; 24 | 25 | SkeletonContentCardContainer.defaultProps = { 26 | columnSizes: HIGHLIGHTS_CARD_GRID_COLUMN_SIZES, 27 | }; 28 | 29 | export default SkeletonContentCardContainer; 30 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/ZeroState/ZeroStateCardFooter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '@openedx/paragon'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const ZeroStateCardFooter = ({ footerClassName, children }) => ( 6 | 7 | {children} 8 | 9 | ); 10 | 11 | ZeroStateCardFooter.propTypes = { 12 | footerClassName: PropTypes.string, 13 | children: PropTypes.node, 14 | }; 15 | 16 | ZeroStateCardFooter.defaultProps = { 17 | footerClassName: undefined, 18 | children: null, 19 | }; 20 | 21 | export default ZeroStateCardFooter; 22 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/ZeroState/ZeroStateCardImage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '@openedx/paragon'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const ZeroStateCardImage = ({ imageContainerClassName, imageClassName, cardImage }) => ( 6 |
7 | 12 |
13 | ); 14 | 15 | ZeroStateCardImage.propTypes = { 16 | imageContainerClassName: PropTypes.string, 17 | imageClassName: PropTypes.string, 18 | cardImage: PropTypes.string.isRequired, 19 | }; 20 | ZeroStateCardImage.defaultProps = { 21 | imageContainerClassName: 'p-4', 22 | imageClassName: undefined, 23 | }; 24 | 25 | export default ZeroStateCardImage; 26 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/ZeroState/ZeroStateCardText.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '@openedx/paragon'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const ZeroStateCardText = ({ textContainerClassName, children }) => ( 6 | 7 | {children} 8 | 9 | ); 10 | 11 | ZeroStateCardText.propTypes = { 12 | textContainerClassName: PropTypes.string, 13 | children: PropTypes.node, 14 | }; 15 | 16 | ZeroStateCardText.defaultProps = { 17 | textContainerClassName: 'text-center', 18 | children: null, 19 | }; 20 | 21 | export default ZeroStateCardText; 22 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/ZeroState/index.js: -------------------------------------------------------------------------------- 1 | import ZeroStateHighlights from './ZeroStateHighlights'; 2 | 3 | export default ZeroStateHighlights; 4 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/data/utils.js: -------------------------------------------------------------------------------- 1 | import { configuration } from '../../../config'; 2 | 3 | // Highlight Card logic for footer text 4 | export const getContentHighlightCardFooter = ({ price, formattedContentType }) => { 5 | if (!price) { 6 | return formattedContentType; 7 | } 8 | return `$${price} · ${formattedContentType}`; 9 | }; 10 | 11 | // Generate URLs for about pages from the enterprise learner portal 12 | export function generateAboutPageUrl({ enterpriseSlug, contentType, contentKey }) { 13 | if (!contentType || !contentKey) { 14 | return undefined; 15 | } 16 | const { ENTERPRISE_LEARNER_PORTAL_URL } = configuration; 17 | const aboutPageBase = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}`; 18 | if (contentType === 'learnerpathway') { 19 | return `${aboutPageBase}/search/${contentKey}`; 20 | } 21 | return `${aboutPageBase}/${contentType}/${contentKey}`; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ContentHighlights/index.js: -------------------------------------------------------------------------------- 1 | import ContentHighlights from './ContentHighlights'; 2 | 3 | export default ContentHighlights; 4 | -------------------------------------------------------------------------------- /src/components/EmailTemplateForm/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export const MODAL_TYPES = { 3 | remind: 'remind', 4 | save: 'save', 5 | assign: 'assign', 6 | revoke: 'revoke', 7 | }; 8 | 9 | export const EMAIL_TEMPLATE_GREETING_ID = 'email-template-greeting'; 10 | export const EMAIL_TEMPLATE_SUBJECT_ID = 'email-template-subject'; 11 | export const EMAIL_TEMPLATE_BODY_ID = 'email-template-body'; 12 | export const EMAIL_TEMPLATE_CLOSING_ID = 'email-template-closing'; 13 | export const EMAIL_TEMPLATE_FILES_ID = 'email-template-files'; 14 | -------------------------------------------------------------------------------- /src/components/EmailTemplateForm/messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from '@edx/frontend-platform/i18n'; 2 | 3 | const messages = defineMessages({ 4 | emailFormName: { 5 | id: 'adminPortal.emailTemplateForm.formName', 6 | defaultMessage: 'Email Template', 7 | }, 8 | emailCustomizeSubject: { 9 | id: 'adminPortal.emailTemplateForm.customizeSubject', 10 | defaultMessage: 'Customize email subject', 11 | }, 12 | emailCustomizeGreeting: { 13 | id: 'adminPortal.emailTemplateForm.customizeGreeting', 14 | defaultMessage: 'Customize greeting', 15 | }, 16 | emailBody: { 17 | id: 'adminPortal.emailTemplateForm.body', 18 | defaultMessage: 'Body', 19 | }, 20 | emailCustomizeClosing: { 21 | id: 'adminPortal.emailTemplateForm.customizeClosing', 22 | defaultMessage: 'Customize closing', 23 | }, 24 | emailAddFiles: { 25 | id: 'adminPortal.emailTemplateForm.addFiles', 26 | defaultMessage: 'add files', 27 | }, 28 | emailMaxFileSizeMessage: { 29 | id: 'adminPortal.emailTemplateForm.maxFileSizeMessage', 30 | defaultMessage: "Max files size shouldn't exceed 250kb.", 31 | }, 32 | }); 33 | 34 | export default messages; 35 | -------------------------------------------------------------------------------- /src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EnrolledLearnersTable renders empty state correctly 1`] = ` 4 |
8 | 11 | 21 | 25 | 26 | 27 |
30 |
33 | There are no results. 34 |
35 |
36 |
37 | `; 38 | -------------------------------------------------------------------------------- /src/components/EnrollmentsTable/__snapshots__/EnrollmentsTable.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EnrollmentsTable renders empty state correctly 1`] = ` 4 |
8 | 11 | 21 | 25 | 26 | 27 |
30 |
33 | There are no results. 34 |
35 |
36 |
37 | `; 38 | -------------------------------------------------------------------------------- /src/components/EnterpriseApp/EnterpriseAppSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from '@openedx/paragon'; 3 | 4 | const EnterpriseAppSkeleton = () => ( 5 | <> 6 |
Loading...
7 | 8 | 9 | 10 | ); 11 | 12 | export default EnterpriseAppSkeleton; 13 | -------------------------------------------------------------------------------- /src/components/EnterpriseApp/EnterpriseAppSkeleton.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import EnterpriseAppSkeleton from './EnterpriseAppSkeleton'; 4 | 5 | describe('EnterpriseAppSkeleton', () => { 6 | it('renders a skeleton', () => { 7 | const tree = renderer 8 | .create(( 9 | 10 | )) 11 | .toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/EnterpriseApp/_EnterpriseApp.scss: -------------------------------------------------------------------------------- 1 | .enterprise-app { 2 | position: relative; 3 | display: flex; 4 | 5 | .content-wrapper { 6 | padding: 10px; 7 | 8 | @include media-breakpoint-up(lg) { 9 | & { 10 | // If we change the width of the icon-only menu links in the Sidebar navigation, 11 | // we will also need to update the padding-left to ensure correct spacing. 12 | padding-left: 51px; 13 | } 14 | } 15 | } 16 | .full-page { 17 | flex: 1; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/EnterpriseApp/__snapshots__/EnterpriseAppSkeleton.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EnterpriseAppSkeleton renders a skeleton 1`] = ` 4 | [ 5 |
8 | Loading... 9 |
, 10 | 14 | 22 | ‌ 23 | 24 |
25 |
, 26 | 30 | 38 | ‌ 39 | 40 |
41 |
, 42 | ] 43 | `; 44 | -------------------------------------------------------------------------------- /src/components/EnterpriseApp/data/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | export const ROUTE_NAMES = { 4 | analytics: 'analytics', 5 | appearance: 'appearance', 6 | bulkEnrollment: 'enrollment', 7 | bulkEnrollmentResults: 'bulk-enrollment-results', 8 | codeManagement: 'coupons', 9 | contentHighlights: 'content-highlights', 10 | learners: 'learners', 11 | learners_v2: 'learners-v2', 12 | learnerCredit: 'learner-credit', 13 | peopleManagement: 'people-management', 14 | reporting: 'reporting', 15 | settings: 'settings', 16 | subscriptionManagement: 'subscriptions', 17 | }; 18 | 19 | export const BUDGET_STATUSES = { 20 | active: 'Active', 21 | expired: 'Expired', 22 | expiring: 'Expiring', 23 | scheduled: 'Scheduled', 24 | retired: 'Retired', 25 | }; 26 | 27 | export const BUDGET_TYPES = { 28 | ecommerce: 'ecommerce', 29 | subsidy: 'subsidy', 30 | policy: 'policy', 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/EnterpriseApp/data/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useEnterpriseCuration } from './useEnterpriseCuration'; 2 | export { default as useEnterpriseCurationContext } from './useEnterpriseCurationContext'; 3 | export { default as useUpdateActiveEnterpriseForUser } from './useUpdateActiveEnterpriseForUser'; 4 | -------------------------------------------------------------------------------- /src/components/EnterpriseList/EnterpriseList.mocks.js: -------------------------------------------------------------------------------- 1 | const generateEnterpriseList = () => { 2 | const enterprises = []; 3 | 4 | for (let i = 1; i <= 20; i += 1) { 5 | enterprises.push({ 6 | uuid: 'ee5e6b3a-069a-4947-bb8d-d2dbc323396c', 7 | name: `Enterprise ${i}`, 8 | slug: `enterprise-${i}`, 9 | }); 10 | } 11 | 12 | return enterprises; 13 | }; 14 | 15 | const mockEnterpriseList = { 16 | count: 20, 17 | current_page: 1, 18 | num_pages: 2, 19 | next: 'next_page_url', 20 | previous: null, 21 | results: generateEnterpriseList(), 22 | start: 0, 23 | }; 24 | 25 | export default mockEnterpriseList; 26 | -------------------------------------------------------------------------------- /src/components/FeatureAnnouncementBanner/FeatureAnnouncementBanner.scss: -------------------------------------------------------------------------------- 1 | img { 2 | width: unset; 3 | height: unset; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/FeatureNotSupportedPage/FeatureNotSupportedPage.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { IntlProvider } from '@edx/frontend-platform/i18n'; 4 | 5 | import FeatureNotSupportedPage from './index'; 6 | 7 | describe('', () => { 8 | it('renders correctly', () => { 9 | const tree = renderer 10 | .create(( 11 | 12 | 13 | 14 | )) 15 | .toJSON(); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/FeatureNotSupportedPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import { FormattedMessage } from '@edx/frontend-platform/i18n'; 4 | import { 5 | Alert, 6 | } from '@openedx/paragon'; 7 | import { 8 | Info, 9 | } from '@openedx/paragon/icons'; 10 | 11 | export const FeatureNotSupported = () => ( 12 | <> 13 | 14 | Feature not supported 15 | 16 |
17 | 18 | 23 | 24 |
25 | 26 | ); 27 | 28 | const FeatureNotSupportedPage = () => ( 29 |
30 |
31 | 32 |
33 |
34 | ); 35 | 36 | export default FeatureNotSupportedPage; 37 | -------------------------------------------------------------------------------- /src/components/FileInput/_FileInput.scss: -------------------------------------------------------------------------------- 1 | .file-input { 2 | input[type='file'] { 3 | border: 0; 4 | clip: rect(0, 0, 0, 0); 5 | height: 1px; 6 | overflow: hidden; 7 | padding: 0; 8 | position: absolute; 9 | white-space: nowrap; 10 | width: 1px; 11 | } 12 | 13 | .file-name, 14 | .btn { 15 | font-size: $font-size-sm; 16 | } 17 | 18 | .has-focus { 19 | box-shadow: $input-btn-focus-box-shadow; 20 | } 21 | 22 | .choose-file-btn, 23 | .remove-file-btn { 24 | text-decoration: underline; 25 | 26 | &:focus { 27 | box-shadow: $input-btn-focus-box-shadow; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/FloatingCollapsible/_FloatingCollapsible.scss: -------------------------------------------------------------------------------- 1 | @import "../../colors"; 2 | 3 | .floating-collapsible { 4 | width: 25rem; 5 | border-radius: 6px; 6 | @include pgn-box-shadow(4, "down"); 7 | 8 | .floating-collapsible__trigger { 9 | background-color: $product-tours-background-color; 10 | color: $white; 11 | } 12 | } -------------------------------------------------------------------------------- /src/components/Footer/Footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | .logo-links { 3 | display: flex; 4 | align-items: center; 5 | 6 | .logo { 7 | padding: 0 1.5rem; 8 | 9 | img { 10 | max-height: 60px; 11 | max-width: 100px; 12 | width: auto; 13 | } 14 | } 15 | } 16 | 17 | .footer-links { 18 | min-width: 410px; 19 | } 20 | } 21 | 22 | // xs 23 | @media (max-width: 767px) { 24 | footer { 25 | .nav { 26 | justify-content: flex-start !important; 27 | margin: 20px 0 0; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Footer/messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from '@edx/frontend-platform/i18n'; 2 | 3 | const messages = defineMessages({ 4 | termsOfService: { 5 | id: 'adminPortal.emailTemplateForm.termsOfService', 6 | defaultMessage: 'Terms of Service', 7 | }, 8 | privacyPolicy: { 9 | id: 'adminPortal.emailTemplateForm.privacyPolicy', 10 | defaultMessage: 'Privacy Policy', 11 | }, 12 | support: { 13 | id: 'adminPortal.emailTemplateForm.support', 14 | defaultMessage: 'Support', 15 | }, 16 | }); 17 | 18 | export default messages; 19 | -------------------------------------------------------------------------------- /src/components/ForbiddenPage/ForbiddenPage.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { IntlProvider } from '@edx/frontend-platform/i18n'; 4 | import { render, screen } from '@testing-library/react'; 5 | import ForbiddenPage from './index'; 6 | import '@testing-library/jest-dom/extend-expect'; 7 | 8 | describe('', () => { 9 | it('renders correctly', () => { 10 | render( 11 | 12 | 13 | , 14 | ); 15 | expect(screen.getByText('403')).toBeInTheDocument(); 16 | expect(screen.getByText('You do not have access to this page.')).toBeInTheDocument(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/ForbiddenPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import { MailtoLink } from '@openedx/paragon'; 4 | 5 | const ForbiddenPage = () => ( 6 |
7 |
8 | 9 | Access Denied 10 | 11 |
12 |

403

13 |

You do not have access to this page.

14 |

15 | For assistance, please contact the edX Customer Success team at 16 | {' '} 17 | customersuccess@edx.org. 18 |

19 |
20 |
21 |
22 | ); 23 | 24 | export default ForbiddenPage; 25 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | .navbar-brand { 3 | img { 4 | max-height: 60px; 5 | max-width: 100px; 6 | width: auto; 7 | } 8 | } 9 | 10 | .user-profile-img { 11 | width: 32px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Hero/_Hero.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | background-repeat: no-repeat; 3 | background-position: center; 4 | background-size: cover; 5 | min-height: 8rem; 6 | display: flex; 7 | align-items: flex-end; 8 | justify-content: space-between; 9 | padding: 1rem; 10 | border-bottom: solid 7px; 11 | 12 | h1 { 13 | margin: 0 14 | } 15 | 16 | img { 17 | max-width: 90px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Img/Img.scss: -------------------------------------------------------------------------------- 1 | img { 2 | width: 100%; 3 | height: auto; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Img/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './Img.scss'; 5 | 6 | const Img = (props) => ( 7 | {props.alt} 8 | ); 9 | 10 | Img.propTypes = { 11 | src: PropTypes.oneOfType([ 12 | PropTypes.string, 13 | PropTypes.object, 14 | ]).isRequired, 15 | alt: PropTypes.string.isRequired, 16 | }; 17 | 18 | export default Img; 19 | -------------------------------------------------------------------------------- /src/components/InfoHover/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { OverlayTrigger, Tooltip } from '@openedx/paragon'; 4 | import { InfoOutline } from '@openedx/paragon/icons'; 5 | 6 | const InfoHover = ({ 7 | className, size, keyName, message, 8 | }) => ( 9 | {message}} 12 | > 13 | 18 | 19 | ); 20 | 21 | InfoHover.defaultProps = { 22 | className: 'float-top', 23 | size: '15px', 24 | }; 25 | 26 | InfoHover.propTypes = { 27 | className: PropTypes.string, 28 | size: PropTypes.string, 29 | keyName: PropTypes.string.isRequired, // pass in a unique, descriptive key for your info hover 30 | message: PropTypes.string.isRequired, 31 | }; 32 | 33 | export default InfoHover; 34 | -------------------------------------------------------------------------------- /src/components/InviteLearnersModal/emailTemplate.js: -------------------------------------------------------------------------------- 1 | import { getSubscriptionContactText } from '../../utils'; 2 | 3 | const emailTemplate = { 4 | greeting: 'Congratulations!', 5 | body: `{ENTERPRISE_NAME} has partnered with edX to give you an unlimited subscription to learn on edX! Take the best courses in the most in-demand subject areas and upskill for a new career opportunity. Earn a professional certificate, start a program or just learn for fun. 6 | {LICENSE_ACTIVATION_LINK} 7 | 8 | About edX 9 | 10 | Since 2012, edX has been committed to increasing access to high-quality education for everyone, everywhere. By harnessing the transformative power of education through online learning, edX empowers learners to unlock their potential and become changemakers. 11 | 12 | We are excited to welcome you to our growing community of over 35 million users and 15 thousand instructors from 160 partner universities and organizations. 13 | `, 14 | closing: getSubscriptionContactText, 15 | }; 16 | 17 | export default emailTemplate; 18 | -------------------------------------------------------------------------------- /src/components/LoadingMessage/_LoadingMessage.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | background: $info-100; 3 | font-size: $font-size-lg; 4 | 5 | &.table-loading, 6 | &.request-codes, 7 | &.support, 8 | &.admin-register, 9 | &.user-activation, 10 | &.subscriptions, 11 | &.settings { 12 | height: 360px; 13 | } 14 | 15 | &.overview, 16 | &.coupons { 17 | height: 180px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/LoadingMessage/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | import { FormattedMessage } from '@edx/frontend-platform/i18n'; 5 | 6 | const LoadingMessage = (props) => { 7 | const { className } = props; 8 | return ( 9 |
15 | Loading... 16 | 17 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | LoadingMessage.propTypes = { 28 | className: PropTypes.string.isRequired, 29 | }; 30 | 31 | export default LoadingMessage; 32 | -------------------------------------------------------------------------------- /src/components/MultipleFileInputField/constants.js: -------------------------------------------------------------------------------- 1 | export const MAX_FILES_SIZE = 256000; 2 | export const FILE_SIZE_EXCEEDS_ERROR = 'Total files size exceeds 250kb'; 3 | -------------------------------------------------------------------------------- /src/components/MultipleFileInputField/utils.js: -------------------------------------------------------------------------------- 1 | export function formatBytes(bytes, decimals = 2) { 2 | if (bytes === 0) { return '0 Bytes'; } 3 | const k = 1024; 4 | const dm = decimals < 0 ? 0 : decimals; 5 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 6 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 7 | return `${parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`; 8 | } 9 | 10 | export function getSizeInBytes(size) { 11 | let sizeOfFiles = size.toLowerCase(); 12 | if (sizeOfFiles.endsWith('bytes')) { 13 | sizeOfFiles = parseFloat(sizeOfFiles); 14 | } else if (sizeOfFiles.endsWith('kb')) { 15 | sizeOfFiles = parseFloat(sizeOfFiles) * 1024; 16 | } else if (sizeOfFiles.endsWith('mb')) { 17 | sizeOfFiles = parseFloat(sizeOfFiles) * (1024 ** 2); 18 | } else { 19 | sizeOfFiles = parseFloat(sizeOfFiles); 20 | } 21 | return sizeOfFiles; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/NewFeatureAlertBrowseAndRequest/data/constants.js: -------------------------------------------------------------------------------- 1 | // Browse and request constants `BrowseAndRequestAlert` 2 | export const BROWSE_AND_REQUEST_ALERT_COOKIE_PREFIX = 'dismissed-browse-and-request-alert'; 3 | export const BROWSE_AND_REQUEST_ALERT_TEXT = 'New! You can now allow all learners to browse' 4 | + ' your catalog and request enrollment to courses.'; 5 | export const REDIRECT_SETTINGS_BUTTON_TEXT = 'Go to settings'; 6 | -------------------------------------------------------------------------------- /src/components/NotFoundPage/NotFoundPage.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { IntlProvider } from '@edx/frontend-platform/i18n'; 4 | 5 | import NotFoundPage from './index'; 6 | 7 | describe('', () => { 8 | it('renders correctly', () => { 9 | const tree = renderer 10 | .create(( 11 | 12 | 13 | 14 | )) 15 | .toJSON(); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/NotFoundPage/__snapshots__/NotFoundPage.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders correctly 1`] = ` 4 |
12 |
15 |
18 |

19 | 404 20 |

21 |

24 | Oops, sorry we can't find that page! 25 |

26 |

27 | Either something went wrong or the page doesn't exist anymore. 28 |

29 |
30 |
31 |
32 | `; 33 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/AddMembersModal/AddMemberModalSummaryDuplicate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, Icon } from '@openedx/paragon'; 3 | import { Error } from '@openedx/paragon/icons'; 4 | 5 | const AddMemberModalSummaryDuplicate = () => ( 6 | 7 | 8 |
9 |
Only 1 invite per email address will be sent.
10 | One or more duplicate emails were detected. Ensure that your entry is correct before proceeding. 11 |
12 |
13 | ); 14 | 15 | export default AddMemberModalSummaryDuplicate; 16 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/AddMembersModal/AddMemberModalSummaryEmptyState.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AddMemberModalSummaryEmptyState = () => ( 4 | <> 5 |
You haven't uploaded any members yet.
6 | Upload a CSV file or select members to get started. 7 | 8 | ); 9 | 10 | export default AddMemberModalSummaryEmptyState; 11 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/AddMembersModal/AddMemberModalSummaryErrorState.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, Icon } from '@openedx/paragon'; 3 | import { Error } from '@openedx/paragon/icons'; 4 | 5 | const AddMemberModalSummaryErrorState = () => ( 6 | 7 | 8 |
9 |
Members can't be added as entered.
10 | Please check your file and try again. 11 |
12 |
13 | ); 14 | 15 | export default AddMemberModalSummaryErrorState; 16 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/GroupDetailPage/AddMemberTableAction.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@openedx/paragon'; 2 | import { Add } from '@openedx/paragon/icons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const AddMemberTableAction = ({ openModal }) => ( 6 | 13 | ); 14 | 15 | AddMemberTableAction.propTypes = { 16 | openModal: PropTypes.func.isRequired, 17 | }; 18 | 19 | export default AddMemberTableAction; 20 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/MemberDetailsCell.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Stack } from '@openedx/paragon'; 3 | 4 | const MemberDetailsCell = ({ row }) => ( 5 | 6 |
7 | {row.original?.enterpriseCustomerUser?.name} 8 |
9 |
10 | {row.original?.enterpriseCustomerUser?.email} 11 |
12 |
13 | ); 14 | 15 | MemberDetailsCell.propTypes = { 16 | row: PropTypes.shape({ 17 | original: PropTypes.shape({ 18 | enterpriseCustomerUser: PropTypes.shape({ 19 | name: PropTypes.string.isRequired, 20 | email: PropTypes.string.isRequired, 21 | }).isRequired, 22 | }).isRequired, 23 | }).isRequired, 24 | }; 25 | 26 | export default MemberDetailsCell; 27 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/MemberJoinedDateCell.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const MemberJoinedDateCell = ({ row }) => ( 4 |
5 | {row.original.enterpriseCustomerUser.joinedOrg} 6 |
7 | ); 8 | 9 | MemberJoinedDateCell.propTypes = { 10 | row: PropTypes.shape({ 11 | original: PropTypes.shape({ 12 | enterpriseCustomerUser: PropTypes.shape({ 13 | joinedOrg: PropTypes.string.isRequired, 14 | }), 15 | }).isRequired, 16 | }).isRequired, 17 | }; 18 | 19 | export default MemberJoinedDateCell; 20 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/RecentActionTableCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import formatDates from './utils'; 4 | 5 | const RecentActionTableCell = ({ 6 | row, 7 | }) => ( 8 |
Added: {formatDates(row.original.activatedAt)}
9 | ); 10 | 11 | RecentActionTableCell.propTypes = { 12 | row: PropTypes.shape({ 13 | original: PropTypes.shape({ 14 | activatedAt: PropTypes.string.isRequired, 15 | }).isRequired, 16 | }).isRequired, 17 | }; 18 | 19 | export default RecentActionTableCell; 20 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/_PeopleManagement.scss: -------------------------------------------------------------------------------- 1 | .group-detail-card { 2 | background-color: $light-200; 3 | .card-button { 4 | justify-content: flex-start !important; 5 | } 6 | .pgn__card-section { 7 | padding: .25rem 1.25rem 1.25rem 1.25rem; 8 | } 9 | } 10 | 11 | .learner-detail-card { 12 | align-items: center; 13 | padding-top: 2rem; 14 | width: 25rem; 15 | background-color: $light-200; 16 | } 17 | .learner-detail-icon { 18 | height: 75px; 19 | width: 75px; 20 | background: black; 21 | border-radius: 50%; 22 | padding: 10px; 23 | color: white; 24 | } 25 | .learner-detail-section { 26 | box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.175); 27 | padding-top: 2rem; 28 | padding-bottom: 1px; 29 | width: 25rem; 30 | border-radius: 10px; 31 | } -------------------------------------------------------------------------------- /src/components/PeopleManagement/data/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useEnterpriseGroupUuid } from './useEnterpriseGroupUuid'; 2 | export { default as useEnterpriseGroupLearnersTableData } from './useEnterpriseGroupLearnersTableData'; 3 | export { default as useEnterpriseMembersTableData } from './useEnterpriseMembersTableData'; 4 | export { default as useAllEnterpriseGroupLearners } from './useAllEnterpriseGroupLearners'; 5 | export { default as useEnterpriseGroupMemberships } from './useEnterpriseGroupMemberships'; 6 | export { default as useLearnerProfileView } from './useLearnerProfileView'; 7 | export { default as useLearnerCreditPlans } from './useLearnerCreditPlans'; 8 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/data/hooks/useAllEnterpriseGroupLearners.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import LmsApiService from '../../../../data/services/LmsApiService'; 4 | import { peopleManagementQueryKeys } from '../../constants'; 5 | 6 | const useAllEnterpriseGroupLearners = (groupUuid) => useQuery({ 7 | queryKey: peopleManagementQueryKeys.learners(groupUuid), 8 | queryFn: () => LmsApiService.fetchAllEnterpriseGroupLearners(groupUuid), 9 | }); 10 | 11 | export default useAllEnterpriseGroupLearners; 12 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/data/hooks/useEnterpriseCourseEnrollments.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { peopleManagementQueryKeys } from '../../constants'; 4 | import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; 5 | 6 | export type EnterpriseCourseEnrollmentArgs = { 7 | userEmail: string, 8 | lmsUserId: string, 9 | enterpriseUuid: string, 10 | }; 11 | 12 | const useEnterpriseCourseEnrollments = ( 13 | { userEmail, lmsUserId, enterpriseUuid } : EnterpriseCourseEnrollmentArgs, 14 | ) => useQuery({ 15 | queryKey: peopleManagementQueryKeys.courseEnrollments({ enterpriseUuid, lmsUserId }), 16 | queryFn: () => EnterpriseAccessApiService.fetchAdminLearnerProfileData(userEmail, lmsUserId, enterpriseUuid), 17 | }); 18 | 19 | export default useEnterpriseCourseEnrollments; 20 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/data/hooks/useEnterpriseGroupMemberships.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import LmsApiService, { EnterpriseGroupMembershipArgs } from '../../../../data/services/LmsApiService'; 4 | import { peopleManagementQueryKeys } from '../../constants'; 5 | 6 | const useEnterpriseGroupMemberships = ({ enterpriseUuid, lmsUserId }: EnterpriseGroupMembershipArgs) => useQuery({ 7 | queryKey: peopleManagementQueryKeys.groupMemberships({ enterpriseUuid, lmsUserId }), 8 | queryFn: () => LmsApiService.fetchEnterpriseGroupMemberships({ enterpriseUuid, lmsUserId }), 9 | }); 10 | 11 | export default useEnterpriseGroupMemberships; 12 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { camelCaseObject } from '@edx/frontend-platform/utils'; 3 | 4 | import LmsApiService from '../../../../data/services/LmsApiService'; 5 | import { peopleManagementQueryKeys } from '../../constants'; 6 | 7 | /** 8 | * Retrieves an enterprise group by the group UUID from the API. 9 | * 10 | * @param {*} queryKey The queryKey from the associated `useQuery` call. 11 | * @returns The enterprise group object 12 | */ 13 | const getEnterpriseGroupUuid = async ({ groupUuid }) => { 14 | const response = await LmsApiService.fetchEnterpriseGroup(groupUuid); 15 | const enterpriseGroup = camelCaseObject(response.data); 16 | return enterpriseGroup; 17 | }; 18 | 19 | const useEnterpriseGroupUuid = (groupUuid, { queryOptions } = {}) => useQuery({ 20 | queryKey: peopleManagementQueryKeys.group(groupUuid), 21 | queryFn: () => getEnterpriseGroupUuid({ groupUuid }), 22 | enabled: !!groupUuid, 23 | ...queryOptions, 24 | }); 25 | 26 | export default useEnterpriseGroupUuid; 27 | -------------------------------------------------------------------------------- /src/components/PeopleManagement/data/hooks/useLearnerProfileView.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import EnterpriseAccessApiService from '../../../../data/services/EnterpriseAccessApiService'; 4 | import { peopleManagementQueryKeys } from '../../constants'; 5 | 6 | interface LearnerProfileViewArgs { 7 | enterpriseUuid: string; 8 | lmsUserId: string; 9 | userEmail: string; 10 | } 11 | 12 | const useLearnerProfileView = ({ 13 | enterpriseUuid, 14 | lmsUserId, 15 | userEmail, 16 | }: LearnerProfileViewArgs) => useQuery({ 17 | queryKey: peopleManagementQueryKeys.learnerProfile({ enterpriseUuid, userId: lmsUserId, userEmail }), 18 | queryFn: () => EnterpriseAccessApiService.fetchAdminLearnerProfileData(userEmail, lmsUserId, enterpriseUuid), 19 | enabled: !!userEmail, 20 | }); 21 | 22 | export default useLearnerProfileView; 23 | -------------------------------------------------------------------------------- /src/components/ProductTours/AdminOnboardingTours/constants.js: -------------------------------------------------------------------------------- 1 | // Admin Tour Targets 2 | const LEARNER_PROGRESS_SIDEBAR = 'learner-progress-sidebar'; 3 | 4 | export const ADMIN_TOUR_TARGETS = { 5 | LEARNER_PROGRESS_SIDEBAR, 6 | }; 7 | 8 | const LEARNER_PROGRESS_ADVANCE_EVENT_NAME = 'edx.ui.enterprise.admin-portal.admin-onboarding-tours.learner-progress.advance'; 9 | const LEARNER_PROGRESS_DISMISS_EVENT_NAME = 'edx.ui.enterprise.admin-portal.admin-onboarding-tours.learner-progress.dismiss'; 10 | 11 | export const ADMIN_TOUR_EVENT_NAMES = { 12 | LEARNER_PROGRESS_ADVANCE_EVENT_NAME, 13 | LEARNER_PROGRESS_DISMISS_EVENT_NAME, 14 | }; 15 | 16 | export const ONBOARDING_WELCOME_MODAL_COOKIE_NAME = 'seen-onboarding-welcome-modal'; 17 | -------------------------------------------------------------------------------- /src/components/ProductTours/_ProductTours.scss: -------------------------------------------------------------------------------- 1 | $white: #FFFFFF; 2 | 3 | .product-tours { 4 | .info-button { 5 | padding: 0; 6 | transform: translate(-1rem, -3rem); 7 | background-color: $white; 8 | 9 | .pgn__icon.btn-icon__icon { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | 14 | .btn-icon__icon-container { 15 | display: default; 16 | align-self: normal; 17 | } 18 | } 19 | } 20 | 21 | .pgn__checkpoint { 22 | border-top: 8px solid #D23228; 23 | } 24 | 25 | .pgn__checkpoint-overlay { 26 | z-index: 1050; 27 | } 28 | 29 | .modal-image { 30 | height: 150px; 31 | margin-bottom: 1rem; 32 | } -------------------------------------------------------------------------------- /src/components/ProductTours/data/utils.js: -------------------------------------------------------------------------------- 1 | import { COOKIE_NAMES } from '../constants'; 2 | 3 | // Filter enabled features prescreened for cookie to populate tour array 4 | export function filterCheckpoints(checkpoints, enabledFeatures) { 5 | const filteredCheckpoints = []; 6 | Object.keys(checkpoints).forEach((checkpoint) => { 7 | if (enabledFeatures[checkpoint]) { 8 | filteredCheckpoints.push(checkpoints[checkpoint]); 9 | } 10 | }); 11 | 12 | return filteredCheckpoints; 13 | } 14 | 15 | // Enable all cookies when onDismiss is called to ensure that the tour is not shown again 16 | export function disableAll() { 17 | // set all cookies to true to ensure that the tour checkpoints are not shown again 18 | Object.keys(COOKIE_NAMES).forEach((key) => { 19 | global.localStorage.setItem(COOKIE_NAMES[key], true); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ReduxFormCheckbox/CheckboxWithTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import ReduxFormCheckbox from '.'; 5 | import IconWithTooltip from '../IconWithTooltip'; 6 | 7 | import './CheckboxWithTooltip.scss'; 8 | 9 | const CheckboxWithTooltip = ({ 10 | className, icon, altText, tooltipText, ...props 11 | }) => ( 12 |
13 | 14 | 19 |
20 | ); 21 | 22 | CheckboxWithTooltip.defaultProps = { 23 | className: '', 24 | }; 25 | 26 | CheckboxWithTooltip.propTypes = { 27 | className: PropTypes.string, 28 | // Icon should be a paragon icon 29 | icon: PropTypes.func.isRequired, 30 | altText: PropTypes.string.isRequired, 31 | tooltipText: PropTypes.string.isRequired, 32 | }; 33 | 34 | export default CheckboxWithTooltip; 35 | -------------------------------------------------------------------------------- /src/components/ReduxFormCheckbox/CheckboxWithTooltip.scss: -------------------------------------------------------------------------------- 1 | .checkbox-with-tooltip { 2 | display: flex; 3 | align-items: center; 4 | .form-group { 5 | margin-bottom: 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ReduxFormCheckbox/ReduxFormCheckbox.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import ReduxFormCheckbox from './index'; 5 | 6 | describe('', () => { 7 | it('renders checked correctly', () => { 8 | const inputProp = { checked: true }; 9 | const component = renderer 10 | .create(( 11 | 12 | )) 13 | .toJSON(); 14 | expect(component).toMatchSnapshot(); 15 | }); 16 | it('renders unchecked correctly', () => { 17 | const inputProp = { checked: false }; 18 | const component = renderer 19 | .create(( 20 | 21 | )) 22 | .toJSON(); 23 | expect(component).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/ReduxFormCheckbox/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form } from '@openedx/paragon'; 4 | 5 | const ReduxFormCheckbox = (props) => { 6 | const { 7 | id, 8 | label, 9 | helpText, 10 | input, 11 | defaultChecked, 12 | } = props; 13 | 14 | return ( 15 |
16 | 23 | {label} 24 | 25 |
26 | ); 27 | }; 28 | 29 | ReduxFormCheckbox.defaultProps = { 30 | helpText: null, 31 | defaultChecked: false, 32 | }; 33 | 34 | ReduxFormCheckbox.propTypes = { 35 | id: PropTypes.string.isRequired, 36 | label: PropTypes.string.isRequired, 37 | input: PropTypes.shape({ 38 | checked: PropTypes.bool, 39 | }).isRequired, 40 | helpText: PropTypes.string, 41 | defaultChecked: PropTypes.bool, 42 | }; 43 | 44 | export default ReduxFormCheckbox; 45 | -------------------------------------------------------------------------------- /src/components/RegisteredLearnersTable/__snapshots__/RegisteredLearnersTable.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RegisteredLearnersTable renders empty state correctly 1`] = ` 4 |
8 | 11 | 21 | 25 | 26 | 27 |
30 |
33 | There are no results. 34 |
35 |
36 |
37 | `; 38 | -------------------------------------------------------------------------------- /src/components/RequestCodesPage/_RequestCodesPage.scss: -------------------------------------------------------------------------------- 1 | .request-codes-form { 2 | label { 3 | font-weight: 600; 4 | } 5 | 6 | .required { 7 | color: $danger; 8 | margin-left: 4px; 9 | } 10 | 11 | input[type='number'] { 12 | width: auto; 13 | } 14 | 15 | .form-cancel-btn { 16 | text-decoration: underline; 17 | 18 | &:focus { 19 | box-shadow: $input-btn-focus-box-shadow; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBar.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { mount } from 'enzyme'; 4 | 5 | import SearchBar from './index'; 6 | 7 | describe('', () => { 8 | let wrapper; 9 | 10 | it('renders correctly', () => { 11 | const tree = renderer 12 | .create(( 13 | {}} 15 | /> 16 | )) 17 | .toJSON(); 18 | expect(tree).toMatchSnapshot(); 19 | }); 20 | 21 | it('calls onSearch callback handler', () => { 22 | const mockOnSearchCallback = jest.fn(); 23 | wrapper = mount(( 24 | 27 | )); 28 | wrapper.find('input[type="text"]').simulate('change', { target: { value: 'foobar' } }); 29 | wrapper.find('form').simulate('submit'); 30 | expect(mockOnSearchCallback).toHaveBeenCalledTimes(1); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { SearchField } from '@openedx/paragon'; 4 | 5 | const SearchBar = props => ( 6 | props.onSearch(query)} 8 | {...props} 9 | /> 10 | ); 11 | 12 | SearchBar.propTypes = { 13 | onSearch: PropTypes.func.isRequired, 14 | }; 15 | 16 | export default SearchBar; 17 | -------------------------------------------------------------------------------- /src/components/SidebarToggle/SidebarToggle.scss: -------------------------------------------------------------------------------- 1 | .sidebar-toggle-btn { 2 | width: 40px; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/SidebarToggle/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button } from '@openedx/paragon'; 4 | import { Close, MenuIcon } from '@openedx/paragon/icons'; 5 | 6 | import './SidebarToggle.scss'; 7 | 8 | const SidebarToggle = (props) => { 9 | const { 10 | isExpandedByToggle, 11 | expandSidebar, 12 | collapseSidebar, 13 | } = props; 14 | 15 | const Icon = isExpandedByToggle ? Close : MenuIcon; 16 | 17 | return ( 18 | 29 | ); 30 | }; 31 | 32 | SidebarToggle.propTypes = { 33 | expandSidebar: PropTypes.func.isRequired, 34 | collapseSidebar: PropTypes.func.isRequired, 35 | isExpandedByToggle: PropTypes.bool.isRequired, 36 | }; 37 | 38 | export default SidebarToggle; 39 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/EmailAddressCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const EmailAddressCell = ({ row }) => ( 5 | {row.original.email} 6 | ); 7 | 8 | EmailAddressCell.propTypes = { 9 | row: PropTypes.shape({ 10 | original: PropTypes.shape({ 11 | email: PropTypes.string, 12 | }), 13 | }).isRequired, 14 | }; 15 | 16 | export default EmailAddressCell; 17 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/RequestDateCell.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { formatTimestamp } from '../../utils'; 4 | 5 | const RequestDateCell = ({ row }) => formatTimestamp({ timestamp: row.original.requestDate }); 6 | 7 | RequestDateCell.propTypes = { 8 | row: PropTypes.shape({ 9 | original: PropTypes.shape({ 10 | requestDate: PropTypes.string, 11 | }), 12 | }).isRequired, 13 | }; 14 | 15 | export default RequestDateCell; 16 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/RequestStatusCell.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Badge } from '@openedx/paragon'; 4 | 5 | import { capitalizeFirstLetter } from '../../utils'; 6 | 7 | const RequestStatusCell = ({ row }) => { 8 | const { requestStatus } = row.original; 9 | 10 | const variant = useMemo( 11 | () => { 12 | const variantsByStatus = { 13 | requested: 'primary', 14 | approved: 'secondary', 15 | error: 'danger', 16 | }; 17 | return variantsByStatus[requestStatus] || 'light'; 18 | }, 19 | [requestStatus], 20 | ); 21 | 22 | return ( 23 | 24 | {capitalizeFirstLetter(row.original.requestStatus)} 25 | 26 | ); 27 | }; 28 | 29 | RequestStatusCell.propTypes = { 30 | row: PropTypes.shape({ 31 | original: PropTypes.shape({ 32 | requestStatus: PropTypes.string, 33 | }), 34 | }).isRequired, 35 | }; 36 | 37 | export default RequestStatusCell; 38 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/data/actions.js: -------------------------------------------------------------------------------- 1 | export const SET_IS_LOADING_SUBSIDY_REQUESTS = 'SET IS LOADING SUBSIDY REQUESTS/OVERVIEW'; 2 | export const setIsLoadingSubsidyRequests = isLoading => ({ 3 | type: SET_IS_LOADING_SUBSIDY_REQUESTS, 4 | payload: { 5 | isLoading, 6 | }, 7 | }); 8 | 9 | export const SET_SUBSIDY_REQUESTS_DATA = 'SET SUBSIDY REQUESTS DATA'; 10 | export const setSubsidyRequestsData = data => ({ 11 | type: SET_SUBSIDY_REQUESTS_DATA, 12 | payload: { 13 | data, 14 | }, 15 | }); 16 | 17 | export const SET_SUBSIDY_REQUESTS_OVERVIEW_DATA = 'SET SUBSIDY REQUESTS OVERVIEW DATA'; 18 | export const setSubsidyRequestsOverviewData = data => ({ 19 | type: SET_SUBSIDY_REQUESTS_OVERVIEW_DATA, 20 | payload: { 21 | data, 22 | }, 23 | }); 24 | 25 | export const UPDATE_SUBSIDY_REQUEST_STATUS = 'UPDATE SUBSIDY REQUEST STATUS'; 26 | export const updateSubsidyRequestStatus = data => ({ 27 | type: UPDATE_SUBSIDY_REQUEST_STATUS, 28 | payload: { 29 | data, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/data/constants.js: -------------------------------------------------------------------------------- 1 | import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; 2 | import { SUPPORTED_SUBSIDY_TYPES } from '../../../data/constants/subsidyRequests'; 3 | 4 | export const DEBOUNCE_TIME_MS = 200; 5 | 6 | export const PAGE_SIZE = 20; 7 | 8 | export const SUBSIDY_REQUESTS_TYPES = { 9 | [SUPPORTED_SUBSIDY_TYPES.coupon]: { 10 | overview: EnterpriseAccessApiService.getCouponCodeRequestOverview, 11 | requests: EnterpriseAccessApiService.getCouponCodeRequests, 12 | }, 13 | [SUPPORTED_SUBSIDY_TYPES.license]: { 14 | overview: EnterpriseAccessApiService.getLicenseRequestOverview, 15 | requests: EnterpriseAccessApiService.getLicenseRequests, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/index.js: -------------------------------------------------------------------------------- 1 | import SubsidyRequestManagementTable from './SubsidyRequestManagementTable'; 2 | 3 | export { 4 | transformRequestOverview, 5 | transformRequests, 6 | } from './data/utils'; 7 | 8 | export { 9 | useSubsidyRequests, 10 | } from './data/hooks'; 11 | 12 | export { 13 | PAGE_SIZE, 14 | } from './data/constants'; 15 | 16 | export default SubsidyRequestManagementTable; 17 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/tests/EmailAddressCell.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import EmailAddressCell from '../EmailAddressCell'; 5 | 6 | const defaultProps = { 7 | row: { 8 | original: { 9 | email: 'test@example.com', 10 | }, 11 | }, 12 | }; 13 | 14 | describe('EmailAddressCell', () => { 15 | test('renders as expected', () => { 16 | const tree = renderer 17 | .create() 18 | .toJSON(); 19 | expect(tree).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/tests/RequestDateCell.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import RequestDateCell from '../RequestDateCell'; 5 | 6 | const defaultProps = { 7 | row: { 8 | original: { 9 | requestDate: '2019-12-03T21:39:24.395101Z', 10 | }, 11 | }, 12 | }; 13 | 14 | describe('RequestDateCell', () => { 15 | test('renders as expected', () => { 16 | const tree = renderer 17 | .create() 18 | .toJSON(); 19 | expect(tree).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/tests/__snapshots__/ActionCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ActionCell does not render anything when request status is not "requested" 1`] = `null`; 4 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/tests/__snapshots__/EmailAddressCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EmailAddressCell renders as expected 1`] = ` 4 | 7 | test@example.com 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/tests/__snapshots__/RequestDateCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RequestDateCell renders as expected 1`] = `"December 3, 2019"`; 4 | -------------------------------------------------------------------------------- /src/components/SubsidyRequestManagementTable/tests/__snapshots__/RequestStatusCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RequestDateCell renders with "approved" status 1`] = ` 4 | 7 | Approved 8 | 9 | `; 10 | 11 | exports[`RequestDateCell renders with "declined" status 1`] = ` 12 | 15 | Declined 16 | 17 | `; 18 | 19 | exports[`RequestDateCell renders with "requested" status 1`] = ` 20 | 23 | Requested 24 | 25 | `; 26 | -------------------------------------------------------------------------------- /src/components/SurveyPage/SurveyPage.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import SurveyPage from './index'; 5 | 6 | describe('', () => { 7 | it('renders correctly', () => { 8 | const tree = renderer 9 | .create(( 10 | 11 | )) 12 | .toJSON(); 13 | expect(tree).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/SurveyPage/__snapshots__/SurveyPage.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders correctly 1`] = ` 4 |
7 | `; 8 | -------------------------------------------------------------------------------- /src/components/SurveyPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { configuration } from '../../config'; 4 | 5 | const SurveyPage = () => { 6 | useEffect(() => { 7 | const widget = document.createElement('script'); 8 | widget.src = configuration.SURVEY_MONKEY_URL; 9 | document.body.appendChild(widget); 10 | }, []); 11 | 12 | return
; 13 | }; 14 | 15 | export default SurveyPage; 16 | -------------------------------------------------------------------------------- /src/components/TableComponent/TableLoadingSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from '@openedx/paragon'; 3 | 4 | const TableLoadingSkeleton = (props) => ( 5 |
6 |
Loading...
7 | 8 |
9 | ); 10 | 11 | export default TableLoadingSkeleton; 12 | -------------------------------------------------------------------------------- /src/components/TableComponent/TableLoadingSkeleton.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import TableLoadingSkeleton from './TableLoadingSkeleton'; 4 | 5 | describe('TableLoadingSkeleton', () => { 6 | it('renders a skeleton', () => { 7 | const tree = renderer 8 | .create(( 9 | 10 | )) 11 | .toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/TableComponent/_TableComponent.scss: -------------------------------------------------------------------------------- 1 | .table { 2 | font-size: $font-size-sm; 3 | 4 | th.sortable > button { 5 | padding-left: 0; 6 | font-size: $font-size-sm; 7 | font-weight: 600; 8 | text-align: left; 9 | background: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/TableLoadingOverlay/TableLoadingOverlay.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { IntlProvider } from '@edx/frontend-platform/i18n'; 4 | import TableLoadingOverlay from '.'; 5 | 6 | describe('TableLoadingOverlay', () => { 7 | it('renders a loading overlay', () => { 8 | const tree = renderer 9 | .create(( 10 | 11 | 12 | 13 | )) 14 | .toJSON(); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/TableLoadingOverlay/_TableLoadingOverlay.scss: -------------------------------------------------------------------------------- 1 | .table-loading-overlay { 2 | position: absolute; 3 | z-index: 10; 4 | background: $black; 5 | opacity: 0.2; 6 | height: 100%; 7 | width: 100%; 8 | } 9 | 10 | .table-loading-message { 11 | position: absolute; 12 | z-index: 11; 13 | width: 100%; 14 | height: 100%; 15 | 16 | span { 17 | padding: 20px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/TableLoadingOverlay/__snapshots__/TableLoadingOverlay.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TableLoadingOverlay renders a loading overlay 1`] = ` 4 | [ 5 |
, 8 |
11 |
14 | Loading... 15 | 18 | Loading 19 | 20 |
21 |
, 22 | ] 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/TableLoadingOverlay/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoadingMessage from '../LoadingMessage'; 3 | 4 | const TableLoadingOverlay = () => ( 5 | <> 6 |
7 |
8 | 9 |
10 | 11 | ); 12 | 13 | export default TableLoadingOverlay; 14 | -------------------------------------------------------------------------------- /src/components/TemplateSourceFields/TemplateSourceFields.scss: -------------------------------------------------------------------------------- 1 | .template-source-fields { 2 | #btn-new-email-template { 3 | &:focus:before { 4 | border-radius: 0.3rem; 5 | } 6 | 7 | border-top-right-radius: 0 !important; 8 | border-bottom-right-radius: 0 !important; 9 | } 10 | 11 | #btn-old-email-template { 12 | &:focus:before { 13 | border-radius: 0.3rem; 14 | } 15 | 16 | border-top-left-radius: 0 !important; 17 | border-bottom-left-radius: 0 !important; 18 | } 19 | 20 | .template-source-btn-wrapper { 21 | width: 100%; 22 | 23 | button { 24 | width: 100%; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/algolia-search/SearchUnavailableAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Alert } from '@openedx/paragon'; 4 | import { Error as ErrorIcon } from '@openedx/paragon/icons'; 5 | 6 | interface SearchUnavailableAlertProps { 7 | className?: string; 8 | } 9 | 10 | const SearchUnavailableAlert: React.FC = ({ className }) => ( 11 | 12 | Search Unavailable 13 |

14 | We're unable to connect to our search service at the moment. 15 | This means search functionality is currently unavailable. 16 |

17 | What you can do: 18 |
    19 |
  • Refresh the page to try again.
  • 20 |
  • Check your network connection.
  • 21 |
  • If the issue persists, please contact your administrator or our support team.
  • 22 |
23 |
24 | ); 25 | 26 | SearchUnavailableAlert.propTypes = { 27 | className: PropTypes.string, 28 | }; 29 | 30 | export default SearchUnavailableAlert; 31 | -------------------------------------------------------------------------------- /src/components/algolia-search/index.ts: -------------------------------------------------------------------------------- 1 | export { default as withAlgoliaSearch } from './withAlgoliaSearch'; 2 | export { default as useAlgoliaSearch, type UseAlgoliaSearchResult } from './useAlgoliaSearch'; 3 | export { default as SearchUnavailableAlert } from './SearchUnavailableAlert'; 4 | -------------------------------------------------------------------------------- /src/components/algolia-search/withAlgoliaSearch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import useAlgoliaSearch from './useAlgoliaSearch'; 5 | 6 | const withAlgoliaSearch = (WrappedComponent) => { 7 | const WithAlgoliaSearch = ({ enterpriseId, enterpriseFeatures, ...rest }) => { 8 | const algolia = useAlgoliaSearch({ 9 | enterpriseId, 10 | enterpriseFeatures, 11 | }); 12 | return ; 13 | }; 14 | WithAlgoliaSearch.propTypes = { 15 | enterpriseId: PropTypes.string.isRequired, 16 | enterpriseFeatures: PropTypes.shape({}).isRequired, 17 | }; 18 | const mapStateToProps = (state) => ({ 19 | enterpriseId: state.portalConfiguration.enterpriseId, 20 | enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, 21 | }); 22 | return connect(mapStateToProps)(WithAlgoliaSearch); 23 | }; 24 | 25 | export default withAlgoliaSearch; 26 | -------------------------------------------------------------------------------- /src/components/forms/_FormWorkflow.scss: -------------------------------------------------------------------------------- 1 | .next-button { 2 | min-width: 60px; 3 | min-height: 40px; 4 | } -------------------------------------------------------------------------------- /src/components/learner-credit-management/AssignmentRecentActionTableCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { formatDate } from './data'; 4 | 5 | const AssignmentRecentActionTableCell = ({ row }) => { 6 | const { original: { recentAction } } = row; 7 | const { actionType, timestamp } = recentAction; 8 | const formattedActionType = `${actionType.charAt(0).toUpperCase()}${actionType.slice(1)}`; 9 | const formattedActionTimestamp = formatDate(timestamp); 10 | return ( 11 | {formattedActionType}: {formattedActionTimestamp} 12 | ); 13 | }; 14 | 15 | AssignmentRecentActionTableCell.propTypes = { 16 | row: PropTypes.shape({ 17 | original: PropTypes.shape({ 18 | recentAction: PropTypes.shape({ 19 | actionType: PropTypes.string.isRequired, 20 | timestamp: PropTypes.string.isRequired, 21 | }).isRequired, 22 | }).isRequired, 23 | }).isRequired, 24 | }; 25 | 26 | export default AssignmentRecentActionTableCell; 27 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/AssignmentRowActionTableCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Stack } from '@openedx/paragon'; 4 | import PendingAssignmentRemindButton from './PendingAssignmentRemindButton'; 5 | import PendingAssignmentCancelButton from './PendingAssignmentCancelButton'; 6 | 7 | const AssignmentRowActionTableCell = ({ row }) => { 8 | const isLearnerStateWaiting = row.original.learnerState === 'waiting'; 9 | return ( 10 | 11 | {isLearnerStateWaiting && ( 12 | 13 | )} 14 | 15 | 16 | ); 17 | }; 18 | 19 | AssignmentRowActionTableCell.propTypes = { 20 | row: PropTypes.shape({ 21 | original: PropTypes.shape({ 22 | learnerEmail: PropTypes.string, 23 | learnerState: PropTypes.string, 24 | uuid: PropTypes.string.isRequired, 25 | }).isRequired, 26 | }).isRequired, 27 | }; 28 | 29 | export default AssignmentRowActionTableCell; 30 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/BudgetDetailPageHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Stack, 4 | } from '@openedx/paragon'; 5 | 6 | import { 7 | useBudgetId, 8 | useSubsidyAccessPolicy, 9 | useEnterpriseOffer, 10 | } from './data'; 11 | 12 | import BudgetDetailPageBreadcrumbs from './BudgetDetailPageBreadcrumbs'; 13 | import BudgetOverviewContent from './BudgetOverviewContent'; 14 | import BudgetExpiryAlertAndModal from '../BudgetExpiryAlertAndModal'; 15 | 16 | const BudgetDetailPageHeader = () => { 17 | const { subsidyAccessPolicyId, enterpriseOfferId } = useBudgetId(); 18 | 19 | const { data: enterpriseOfferMetadata } = useEnterpriseOffer(enterpriseOfferId); 20 | const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); 21 | 22 | const displayName = subsidyAccessPolicy?.displayName || enterpriseOfferMetadata?.displayName || 'Overview'; 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default (BudgetDetailPageHeader); 34 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/CustomDataTableEmptyState.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { DataTable, DataTableContext } from '@openedx/paragon'; 3 | import { useIntl } from '@edx/frontend-platform/i18n'; 4 | 5 | const CustomDataTableEmptyState = () => { 6 | const intl = useIntl(); 7 | const { isLoading } = useContext(DataTableContext); 8 | if (isLoading) { 9 | return null; 10 | } 11 | return ( 12 | 21 | ); 22 | }; 23 | 24 | export default CustomDataTableEmptyState; 25 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/LearnerCreditDisclaimer.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Icon, Col, Stack, 3 | } from '@openedx/paragon'; 4 | import { Info } from '@openedx/paragon/icons'; 5 | import PropTypes from 'prop-types'; 6 | 7 | const LearnerCreditDisclaimer = ({ offerLastUpdated }) => ( 8 | 9 | 10 | 11 | Data last updated on {offerLastUpdated}. This data reflects 12 | the current active learner credit only and does not include 13 | other spend by your organization (codes, manual enrollments, past learner credit plans). 14 | 15 | 16 | ); 17 | 18 | LearnerCreditDisclaimer.propTypes = { 19 | offerLastUpdated: PropTypes.string.isRequired, 20 | }; 21 | 22 | export default LearnerCreditDisclaimer; 23 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/SpendTableAmountContents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Stack } from '@openedx/paragon'; 4 | 5 | import { formatPrice } from './data'; 6 | 7 | const SpendTableAmountContents = ({ row }) => { 8 | const formattedContentPrice = formatPrice(row.original.courseListPrice); 9 | return ( 10 | 11 | {row.original.reversal && ( 12 |
+{formattedContentPrice}
13 | )} 14 |
-{formattedContentPrice}
15 |
16 | ); 17 | }; 18 | 19 | const rowPropType = PropTypes.shape({ 20 | original: PropTypes.shape({ 21 | courseListPrice: PropTypes.number.isRequired, 22 | reversal: PropTypes.shape({ 23 | created: PropTypes.string, 24 | }), 25 | }).isRequired, 26 | }).isRequired; 27 | 28 | SpendTableAmountContents.propTypes = { 29 | row: rowPropType, 30 | }; 31 | 32 | export default SpendTableAmountContents; 33 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryEmptyState.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from '@edx/frontend-platform/i18n'; 3 | 4 | const AssignmentModalSummaryEmptyState = () => ( 5 | <> 6 |
7 | 12 |
13 | 14 | 19 | 20 | 21 | ); 22 | 23 | export default AssignmentModalSummaryEmptyState; 24 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/cards/CourseCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import BaseCourseCard from './BaseCourseCard'; 5 | import CourseCardFooterActions from './CourseCardFooterActions'; 6 | 7 | const CourseCard = ({ original }) => ( 8 | 12 | ); 13 | 14 | CourseCard.propTypes = { 15 | original: PropTypes.shape().isRequired, // pass-thru prop to `BaseCourseCard` 16 | }; 17 | 18 | export default CourseCard; 19 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/cards/data/constants.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const commonErrorAlertModalPropTypes = { 4 | isErrorModalOpen: PropTypes.bool.isRequired, 5 | closeErrorModal: PropTypes.func.isRequired, 6 | closeAssignmentModal: PropTypes.func.isRequired, 7 | }; 8 | 9 | export const MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT = 15; 10 | 11 | export const MAX_EMAIL_ENTRY_LIMIT = 1000; 12 | 13 | export const EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY = 1000; 14 | 15 | export const INPUT_TYPE = { 16 | EMAIL: 'email', 17 | CSV: 'csv', 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/cards/data/index.js: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './utils'; 3 | export { default as useCourseCardMetadata } from './useCourseCardMetadata'; 4 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useAllFlexEnterpriseGroups.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { learnerCreditManagementQueryKeys } from '../constants'; 4 | import LmsApiService from '../../../../data/services/LmsApiService'; 5 | import { fetchPaginatedData } from '../../../../data/services/apiServiceUtils'; 6 | 7 | /** 8 | * Retrieves all enterprise groups associated with the organization 9 | * 10 | * @param {*} queryKey The queryKey from the associated `useQuery` call. 11 | * @returns The enterprise group object 12 | */ 13 | export const getAllFlexEnterpriseGroups = async ({ enterpriseId }) => { 14 | const { results } = await fetchPaginatedData(`${LmsApiService.enterpriseGroupListUrl}?enterprise_uuids=${enterpriseId}`); 15 | return results.filter(result => result.groupType === 'flex'); 16 | }; 17 | 18 | const useAllFlexEnterpriseGroups = (enterpriseId, { queryOptions } = {}) => useQuery({ 19 | queryKey: learnerCreditManagementQueryKeys.group(enterpriseId), 20 | queryFn: () => getAllFlexEnterpriseGroups({ enterpriseId }), 21 | ...queryOptions, 22 | }); 23 | 24 | export default useAllFlexEnterpriseGroups; 25 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useBudgetDetailActivityOverview.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { retrieveBudgetDetailActivityOverview } from '../utils'; 3 | import useBudgetId from './useBudgetId'; 4 | import useSubsidyAccessPolicy from './useSubsidyAccessPolicy'; 5 | import { learnerCreditManagementQueryKeys } from '../constants'; 6 | 7 | const useBudgetDetailActivityOverview = ({ enterpriseUUID, isTopDownAssignmentEnabled }) => { 8 | const { budgetId, subsidyAccessPolicyId } = useBudgetId(); 9 | const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); 10 | return useQuery({ 11 | queryKey: learnerCreditManagementQueryKeys.budgetActivityOverview(budgetId), 12 | queryFn: (args) => retrieveBudgetDetailActivityOverview({ 13 | ...args, 14 | budgetId, 15 | subsidyAccessPolicy, 16 | enterpriseUUID, 17 | isTopDownAssignmentEnabled, 18 | }), 19 | }); 20 | }; 21 | 22 | export default useBudgetDetailActivityOverview; 23 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useBudgetId.js: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | import { isUUID } from '../utils'; 3 | 4 | /** 5 | * Given a page route with the `:budgetId` param, returns the `budgetId` and either a 6 | * `enterpriseOfferId` or `subsidyAccessPolicyId` depending on the type of budget, as determined 7 | * by whether the `budgetId` is a UUID or integer. This is necessary as the Learner Credit Management 8 | * feature currently supports both enterprise offers AND subsidy access policies, but may rely on different 9 | * API data sources depending on whether the budget is an enterprise offer or subsidy access policy. 10 | * 11 | * @returns An object containing the `budgetId` from the URL params, as well as the 12 | * enterpriseOfferId or subsidyAccessPolicyId. 13 | */ 14 | const useBudgetId = () => { 15 | const { budgetId } = useParams(); 16 | const enterpriseOfferId = isUUID(budgetId) ? null : budgetId; 17 | const subsidyAccessPolicyId = isUUID(budgetId) ? budgetId : null; 18 | return { 19 | budgetId, 20 | enterpriseOfferId, 21 | subsidyAccessPolicyId, 22 | }; 23 | }; 24 | 25 | export default useBudgetId; 26 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useContentMetadata.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { camelCaseObject } from '@edx/frontend-platform/utils'; 3 | 4 | import { learnerCreditManagementQueryKeys } from '../constants'; 5 | import EnterpriseCatalogApiService from '../../../../data/services/EnterpriseCatalogApiService'; 6 | 7 | const getContentMetadata = async ({ catalogUuid }) => { 8 | const response = await EnterpriseCatalogApiService.fetchEnterpriseCatalogMetadata({ catalogUuid }); 9 | const contentMetadata = camelCaseObject(response.data); 10 | return contentMetadata; 11 | }; 12 | 13 | const useContentMetadata = (catalogUuid, { queryOptions } = {}) => useQuery({ 14 | queryKey: learnerCreditManagementQueryKeys.group(catalogUuid), 15 | queryFn: () => getContentMetadata({ catalogUuid }), 16 | ...queryOptions, 17 | }); 18 | 19 | export default useContentMetadata; 20 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useEnterpriseCustomer.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { camelCaseObject } from '@edx/frontend-platform/utils'; 3 | 4 | import { learnerCreditManagementQueryKeys } from '../constants'; 5 | import LmsApiService from '../../../../data/services/LmsApiService'; 6 | 7 | /** 8 | * Retrieves a enterprise customer by UUID from the API. 9 | * 10 | * @param {*} queryKey The queryKey from the associated `useQuery` call. 11 | * @returns The enterprise customer object 12 | */ 13 | const getEnterpriseCustomer = async (enterpriseCustomerUuid) => { 14 | const response = await LmsApiService.fetchEnterpriseCustomer(enterpriseCustomerUuid); 15 | const enterpriseCustomer = camelCaseObject(response.data); 16 | return enterpriseCustomer; 17 | }; 18 | 19 | const useEnterpriseCustomer = (enterpriseCustomerUuid, { queryOptions } = {}) => useQuery({ 20 | queryKey: learnerCreditManagementQueryKeys.enterpriseCustomer(enterpriseCustomerUuid), 21 | queryFn: () => getEnterpriseCustomer(enterpriseCustomerUuid), 22 | ...queryOptions, 23 | }); 24 | 25 | export default useEnterpriseCustomer; 26 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useEnterpriseGroup.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { isEmpty } from 'lodash-es'; 3 | 4 | import { learnerCreditManagementQueryKeys } from '../constants'; 5 | import LmsApiService from '../../../../data/services/LmsApiService'; 6 | import { SubsidyAccessPolicy } from '../types'; 7 | 8 | /** 9 | * Retrieves a enterprise group by the policy the from the API. 10 | * @returns The enterprise group object 11 | */ 12 | const getEnterpriseGroup = async ({ subsidyAccessPolicy }: { subsidyAccessPolicy?: SubsidyAccessPolicy }) => { 13 | if (!subsidyAccessPolicy || isEmpty(subsidyAccessPolicy.groupAssociations)) { 14 | return null; 15 | } 16 | const response = await LmsApiService.fetchEnterpriseGroup(subsidyAccessPolicy.groupAssociations[0]); 17 | return response.data; 18 | }; 19 | 20 | const useEnterpriseGroup = (subsidyAccessPolicy: SubsidyAccessPolicy | undefined) => useQuery({ 21 | queryKey: learnerCreditManagementQueryKeys.group(subsidyAccessPolicy?.uuid), 22 | queryFn: () => getEnterpriseGroup({ subsidyAccessPolicy }), 23 | }); 24 | 25 | export default useEnterpriseGroup; 26 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useEnterpriseGroupLearners.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { camelCaseObject } from '@edx/frontend-platform/utils'; 3 | 4 | import LmsApiService from '../../../../data/services/LmsApiService'; 5 | import { learnerCreditManagementQueryKeys } from '../constants'; 6 | 7 | const getEnterpriseGroupLearners = async ({ queryKey }) => { 8 | const subsidyAccessPolicyUUID = queryKey[2]; 9 | const response = await LmsApiService.fetchEnterpriseGroupLearners(subsidyAccessPolicyUUID); 10 | const enterpriseGroupLearners = camelCaseObject(response.data); 11 | return enterpriseGroupLearners; 12 | }; 13 | 14 | const useEnterpriseGroupLearners = (enterpriseGroupUuid, { queryOptions } = {}) => useQuery({ 15 | queryKey: learnerCreditManagementQueryKeys.budgetGroupLearners(enterpriseGroupUuid), 16 | queryFn: getEnterpriseGroupLearners, 17 | enabled: !!enterpriseGroupUuid, 18 | ...queryOptions, 19 | }); 20 | 21 | export default useEnterpriseGroupLearners; 22 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useEnterpriseOffer.js: -------------------------------------------------------------------------------- 1 | // modify the query keys map to include a queryKey for `budgetEnterpriseOffer` that depends on `.budget()`. 2 | import { useQuery } from '@tanstack/react-query'; 3 | import { camelCaseObject } from '@edx/frontend-platform/utils'; 4 | import EcommerceApiService from '../../../../data/services/EcommerceApiService'; 5 | import { learnerCreditManagementQueryKeys } from '../constants'; 6 | 7 | const getEnterpriseOffer = async ({ queryKey }) => { 8 | const enterpriseOfferId = queryKey[2]; 9 | const response = await EcommerceApiService.fetchEnterpriseOffer(enterpriseOfferId); 10 | return camelCaseObject(response.data); 11 | }; 12 | 13 | // Hook to fetch an individual enterprise offer from ecommerce. 14 | const useEnterpriseOffer = (enterpriseOfferId) => useQuery({ 15 | queryKey: learnerCreditManagementQueryKeys.budgetEnterpriseOffer(enterpriseOfferId), 16 | queryFn: getEnterpriseOffer, 17 | enabled: !!enterpriseOfferId, 18 | }); 19 | 20 | export default useEnterpriseOffer; 21 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/useIsLargeOrGreater.js: -------------------------------------------------------------------------------- 1 | import { breakpoints, useMediaQuery } from '@openedx/paragon'; 2 | 3 | const useIsLargeOrGreater = () => useMediaQuery({ query: `(min-width: ${breakpoints.large.minWidth}px)` }); 4 | 5 | export default useIsLargeOrGreater; 6 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js: -------------------------------------------------------------------------------- 1 | import { useParams, generatePath } from 'react-router-dom'; 2 | 3 | import useBudgetId from './useBudgetId'; 4 | 5 | import { LEARNER_CREDIT_ROUTE } from '../constants'; 6 | 7 | const usePathToCatalogTab = () => { 8 | const { budgetId } = useBudgetId(); 9 | const { enterpriseSlug, enterpriseAppPage } = useParams(); 10 | const pathToCatalogTab = generatePath(LEARNER_CREDIT_ROUTE, { 11 | enterpriseSlug, enterpriseAppPage, budgetId, activeTabKey: 'catalog', 12 | }); 13 | return pathToCatalogTab; 14 | }; 15 | 16 | export default usePathToCatalogTab; 17 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/index.js: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './utils'; 3 | export * from './hooks'; 4 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/tests/constants.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | TEST_FLAG, 3 | ENABLE_TESTING, 4 | testEnterpriseCatalogUuid, 5 | } from '../constants'; 6 | 7 | const enterpriseCatalogUuid = 'test-enterprise-catalogUuid'; 8 | 9 | describe('constants', () => { 10 | it('should be defined', () => { 11 | expect(testEnterpriseCatalogUuid).toBeDefined(); 12 | }); 13 | it('ENABLE_TESTING should pass through when the TEST_FLAG = false', () => { 14 | expect(TEST_FLAG).toBe(false); 15 | expect(ENABLE_TESTING(enterpriseCatalogUuid)).toBe(enterpriseCatalogUuid); 16 | }); 17 | it('ENABLE_TESTING should return the testEnterpriseId when passing true parameter', () => { 18 | expect(ENABLE_TESTING(testEnterpriseCatalogUuid, true)).toBe(testEnterpriseCatalogUuid); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/data/types.ts: -------------------------------------------------------------------------------- 1 | export type SubsidyAccessPolicy = { 2 | uuid: string; 3 | displayName: string; 4 | isAssignable: boolean; 5 | policyType: string; 6 | assignmentConfiguration: Record; 7 | catalogUuid: string; 8 | groupAssociations: string[]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import MultipleBudgetsPage from './MultipleBudgetsPage'; 4 | import BudgetDetailPage from './BudgetDetailPage'; 5 | 6 | const LearnerCreditManagementRoutes = () => ( 7 |
8 | 9 | } 12 | /> 13 | 14 | } 17 | /> 18 | 19 |
20 | ); 21 | 22 | export default LearnerCreditManagementRoutes; 23 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/invite-modal/InviteModalSummaryDuplicate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, Icon } from '@openedx/paragon'; 3 | import { Error } from '@openedx/paragon/icons'; 4 | 5 | const InviteModalSummaryDuplicate = () => ( 6 | 7 | 8 |
9 |
Only 1 invite per email address will be sent.
10 | One or more duplicate emails were detected. Ensure that your entry is correct before proceeding. 11 |
12 |
13 | ); 14 | 15 | export default InviteModalSummaryDuplicate; 16 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/invite-modal/InviteModalSummaryEmptyState.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const InviteModalSummaryEmptyState = ({ isGroupInvite }) => { 5 | if (isGroupInvite) { 6 | return ( 7 | <> 8 |
You haven't uploaded any members yet.
9 | Upload a CSV file or select members to get started. 10 | 11 | ); 12 | } 13 | return ( 14 | <> 15 |
You haven't entered any members yet.
16 | Add member emails to get started. 17 | 18 | ); 19 | }; 20 | 21 | InviteModalSummaryEmptyState.propTypes = { 22 | isGroupInvite: PropTypes.bool, 23 | }; 24 | 25 | export default InviteModalSummaryEmptyState; 26 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/invite-modal/InviteModalSummaryErrorState.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, Icon } from '@openedx/paragon'; 3 | import { Error } from '@openedx/paragon/icons'; 4 | 5 | const InviteModalSummaryErrorState = () => ( 6 | 7 | 8 |
9 |
Members can't be invited as entered.
10 | Please check your member emails and try again. 11 |
12 |
13 | ); 14 | 15 | export default InviteModalSummaryErrorState; 16 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/invite-modal/InviteSummaryCount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card } from '@openedx/paragon'; 4 | import { LearnerEmailsValidityReport } from '../cards/data'; 5 | 6 | type InviteSummaryCountProps = { 7 | memberInviteMetadata: LearnerEmailsValidityReport 8 | }; 9 | 10 | const InviteSummaryCount = ({ memberInviteMetadata }: InviteSummaryCountProps) => ( 11 | 12 | 13 | 14 | Total members to add 15 | 16 | {memberInviteMetadata?.validatedEmails?.length} 17 | 18 | 19 | ); 20 | 21 | InviteSummaryCount.propTypes = { 22 | memberInviteMetadata: PropTypes.shape({ 23 | validatedEmails: PropTypes.arrayOf(PropTypes.string), 24 | }).isRequired, 25 | }; 26 | 27 | export default InviteSummaryCount; 28 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/requests-tab/AmountCell.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { formatPrice } from '../data'; 3 | 4 | const AmountCell = ({ row }) => { 5 | const formattedContentPrice = formatPrice(row.original.amount / 100); 6 | return ( 7 |
-{formattedContentPrice}
8 | ); 9 | }; 10 | 11 | AmountCell.propTypes = { 12 | row: PropTypes.shape({ 13 | original: PropTypes.shape({ 14 | amount: PropTypes.number.isRequired, 15 | }).isRequired, 16 | }).isRequired, 17 | }; 18 | 19 | export default AmountCell; 20 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/requests-tab/data/constants.js: -------------------------------------------------------------------------------- 1 | export const PAGE_SIZE = 4; 2 | export const REQUEST_TAB_VISIBLE_STATES = [ 3 | 'requested', 4 | 'declined', 5 | 'cancelled', 6 | 'errored', 7 | ]; 8 | 9 | export const REQUEST_STATUS_FILTER_CHOICES = [ 10 | { 11 | name: 'Requested', 12 | value: 'requested', 13 | }, 14 | { 15 | name: 'Declined', 16 | value: 'declined', 17 | }, 18 | { 19 | name: 'Cancelled', 20 | value: 'cancelled', 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/components/learner-credit-management/tests/LearnerCreditDisclaimer.test.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | screen, 3 | render, 4 | } from '@testing-library/react'; 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import LearnerCreditDisclaimer from '../LearnerCreditDisclaimer'; 7 | 8 | describe('', () => { 9 | it('renders', () => { 10 | render(); 11 | expect(screen.getByText('Data last updated on February 20th, 2022. This data reflects', { exact: false })).toBeInTheDocument(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/settings/HelpCenterButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Hyperlink } from '@openedx/paragon'; 3 | import PropTypes from 'prop-types'; 4 | import { FormattedMessage } from '@edx/frontend-platform/i18n'; 5 | 6 | const HelpCenterButton = ({ 7 | url, 8 | children, 9 | ...rest 10 | }) => { 11 | const destinationUrl = url; 12 | 13 | return ( 14 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | HelpCenterButton.defaultProps = { 26 | children: ( 27 | 32 | ), 33 | }; 34 | 35 | HelpCenterButton.propTypes = { 36 | children: PropTypes.node, 37 | url: PropTypes.string, 38 | }; 39 | 40 | export default HelpCenterButton; 41 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/DateCreatedTableCell.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { formatTimestamp } from '../../../utils'; 5 | 6 | const DateCreatedTableCell = ({ row }) => { 7 | const { created } = row.original; 8 | const formattedDateCreated = useMemo(() => formatTimestamp({ timestamp: created }), [created]); 9 | return formattedDateCreated; 10 | }; 11 | 12 | DateCreatedTableCell.propTypes = { 13 | row: PropTypes.shape({ 14 | original: PropTypes.shape({ 15 | created: PropTypes.string, 16 | }), 17 | }).isRequired, 18 | }; 19 | 20 | export default DateCreatedTableCell; 21 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/LinkCopiedToast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Toast } from '@openedx/paragon'; 3 | import { FormattedMessage } from '@edx/frontend-platform/i18n'; 4 | 5 | const LinkCopiedToast = (props) => ( 6 | 7 | 12 | 13 | ); 14 | 15 | export default LinkCopiedToast; 16 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/LinkTableCell.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import getInviteURL from './utils'; 4 | 5 | const LinkTableCell = ({ row, enterpriseSlug }) => { 6 | const { uuid } = row.original; 7 | return getInviteURL(enterpriseSlug, uuid); 8 | }; 9 | 10 | LinkTableCell.propTypes = { 11 | row: PropTypes.shape({ 12 | original: PropTypes.shape({ 13 | uuid: PropTypes.string, 14 | }), 15 | }).isRequired, 16 | enterpriseSlug: PropTypes.string.isRequired, 17 | }; 18 | 19 | export default LinkTableCell; 20 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/StatusTableCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Badge } from '@openedx/paragon'; 4 | import { FormattedMessage } from '@edx/frontend-platform/i18n'; 5 | 6 | const StatusTableCell = ({ row }) => { 7 | const { isValid } = row.original; 8 | return ( 9 | 10 | {isValid ? ( 11 | 16 | ) : ( 17 | 22 | )} 23 | 24 | ); 25 | }; 26 | 27 | StatusTableCell.propTypes = { 28 | row: PropTypes.shape({ 29 | original: PropTypes.shape({ 30 | isValid: PropTypes.bool, 31 | }), 32 | }).isRequired, 33 | }; 34 | 35 | export default StatusTableCell; 36 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/UsageTableCell.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const UsageTableCell = ({ row }) => { 4 | const { usageCount, usageLimit } = row.original; 5 | return `${usageCount.toLocaleString()} of ${usageLimit.toLocaleString()}`; 6 | }; 7 | 8 | UsageTableCell.propTypes = { 9 | row: PropTypes.shape({ 10 | original: PropTypes.shape({ 11 | usageCount: PropTypes.number, 12 | usageLimit: PropTypes.number, 13 | }), 14 | }).isRequired, 15 | }; 16 | 17 | export default UsageTableCell; 18 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/DateCreatedTableCell.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import DateCreatedTableCell from '../DateCreatedTableCell'; 5 | 6 | describe('DateCreatedTableCell', () => { 7 | it('renders correctly', () => { 8 | const props = { 9 | row: { 10 | original: { 11 | created: '2022-01-10T12:00:00Z', 12 | }, 13 | }, 14 | }; 15 | const tree = renderer 16 | .create() 17 | .toJSON(); 18 | expect(tree).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/LinkTableCell.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import LinkTableCell from '../LinkTableCell'; 5 | 6 | jest.mock('@edx/frontend-platform/config', () => ({ 7 | getConfig: () => ({ ENTERPRISE_LEARNER_PORTAL_URL: 'http://localhost:8734' }), 8 | })); 9 | 10 | describe('LinkTableCell', () => { 11 | it('renders correctly', () => { 12 | const props = { 13 | row: { 14 | original: { 15 | uuid: 'test-invite-key-uuid', 16 | }, 17 | }, 18 | enterpriseSlug: 'test-enterprise', 19 | }; 20 | const tree = renderer 21 | .create() 22 | .toJSON(); 23 | expect(tree).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/SettingsAccessConfiguredSubsidyType.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | screen, 4 | } from '@testing-library/react'; 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import SettingsAccessConfiguredSubsidyType from '../SettingsAccessConfiguredSubsidyType'; 7 | import { SUPPORTED_SUBSIDY_TYPES } from '../../../../data/constants/subsidyRequests'; 8 | import { renderWithI18nProvider } from '../../../test/testUtils'; 9 | 10 | describe('', () => { 11 | it('renders correctly', () => { 12 | renderWithI18nProvider(); 13 | expect(screen.getByText('Licenses')).toBeInTheDocument(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/StatusTableCell.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import { IntlProvider } from '@edx/frontend-platform/i18n'; 5 | import StatusTableCell from '../StatusTableCell'; 6 | 7 | const StatusTableCellWrapper = (props) => ( 8 | 9 | 10 | 11 | ); 12 | 13 | describe('StatusTableCell', () => { 14 | it('renders valid status correctly', () => { 15 | const props = { 16 | row: { 17 | original: { 18 | isValid: true, 19 | }, 20 | }, 21 | }; 22 | const tree = renderer 23 | .create() 24 | .toJSON(); 25 | expect(tree).toMatchSnapshot(); 26 | }); 27 | 28 | it('renders invalid status correctly', () => { 29 | const props = { 30 | row: { 31 | original: { 32 | isValid: false, 33 | }, 34 | }, 35 | }; 36 | const tree = renderer 37 | .create() 38 | .toJSON(); 39 | expect(tree).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/UsageTableCell.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import UsageTableCell from '../UsageTableCell'; 5 | 6 | describe('UsageTableCell', () => { 7 | it('renders correctly', () => { 8 | const props = { 9 | row: { 10 | original: { 11 | usageCount: 10, 12 | usageLimit: 100, 13 | }, 14 | }, 15 | }; 16 | const tree = renderer 17 | .create() 18 | .toJSON(); 19 | expect(tree).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/__snapshots__/DateCreatedTableCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DateCreatedTableCell renders correctly 1`] = `"January 10, 2022"`; 4 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/__snapshots__/LinkTableCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LinkTableCell renders correctly 1`] = `"http://localhost:8734/test-enterprise/invite/test-invite-key-uuid"`; 4 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/__snapshots__/StatusTableCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`StatusTableCell renders invalid status correctly 1`] = ` 4 | 7 | Inactive 8 | 9 | `; 10 | 11 | exports[`StatusTableCell renders valid status correctly 1`] = ` 12 | 15 | Active 16 | 17 | `; 18 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/tests/__snapshots__/UsageTableCell.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UsageTableCell renders correctly 1`] = `"10 of 100"`; 4 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAccessTab/utils.js: -------------------------------------------------------------------------------- 1 | import { getConfig } from '@edx/frontend-platform/config'; 2 | 3 | export default function getInviteURL(enterpriseSlug, inviteUUID) { 4 | return `${getConfig().ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/invite/${inviteUUID}`; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/settings/SettingsApiCredentialsTab/Context.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const ErrorContext = createContext(null); 4 | export const ShowSuccessToast = createContext(null); 5 | export const EnterpriseId = createContext(null); 6 | -------------------------------------------------------------------------------- /src/components/settings/SettingsApiCredentialsTab/CopiedToast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Toast } from '@openedx/paragon'; 4 | 5 | const CopiedToast = ({ content, ...rest }) => ( 6 | 7 | {content} 8 | 9 | ); 10 | CopiedToast.propTypes = { 11 | content: PropTypes.string.isRequired, 12 | }; 13 | export default CopiedToast; 14 | -------------------------------------------------------------------------------- /src/components/settings/SettingsApiCredentialsTab/FailedAlert.jsx: -------------------------------------------------------------------------------- 1 | import { Alert } from '@openedx/paragon'; 2 | import { Error } from '@openedx/paragon/icons'; 3 | import { credentialErrorMessage } from './constants'; 4 | 5 | const FailedAlert = () => ( 6 | 7 | 8 | Credential generation failed 9 | 10 |

11 | {credentialErrorMessage} 12 |

13 |
14 | ); 15 | 16 | export default FailedAlert; 17 | -------------------------------------------------------------------------------- /src/components/settings/SettingsApiCredentialsTab/constants.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const dataPropType = PropTypes.shape({ 4 | name: PropTypes.string, 5 | redirect_uris: PropTypes.string, 6 | client_id: PropTypes.string, 7 | client_secret: PropTypes.string, 8 | api_client_documentation: PropTypes.string, 9 | updated: PropTypes.bool, 10 | }); 11 | 12 | export const credentialErrorMessage = 'Something went wrong while ' 13 | + 'generating your credentials. Please try again. ' 14 | + 'If the issue continues, contact Enterprise Customer Support.'; 15 | -------------------------------------------------------------------------------- /src/components/settings/SettingsAppearanceTab/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ColorAccessibilityChecker = (color: string, textColor: Color) => boolean; 2 | 3 | export type Theme = { 4 | title: string, 5 | button: string, 6 | banner: string, 7 | accent: string, 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLMSTab/LMSConfigs/Blackboard/BlackboardTypes.tsx: -------------------------------------------------------------------------------- 1 | export type BlackboardConfigCamelCase = { 2 | lms: string; 3 | blackboardAccountId: string; 4 | blackboardBaseUrl: string; 5 | displayName: string; 6 | clientId: string; 7 | clientSecret: string; 8 | id: string; 9 | active: boolean; 10 | uuid: string; 11 | refreshToken: string; 12 | }; 13 | 14 | export type BlackboardConfigSnakeCase = { 15 | lms: string; 16 | blackboard_base_url: string; 17 | display_name: string; 18 | id: string; 19 | active: boolean; 20 | uuid: string; 21 | enterprise_customer: string; 22 | refresh_token: string; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLMSTab/LMSConfigs/Canvas/CanvasTypes.tsx: -------------------------------------------------------------------------------- 1 | export type CanvasConfigCamelCase = { 2 | lms: string; 3 | canvasAccountId: string; 4 | canvasBaseUrl: string; 5 | displayName: string; 6 | clientId: string; 7 | clientSecret: string; 8 | id: string; 9 | active: boolean; 10 | uuid: string; 11 | refreshToken: string; 12 | }; 13 | 14 | export type CanvasConfigSnakeCase = { 15 | lms: string; 16 | canvas_account_id: string; 17 | canvas_base_url: string; 18 | display_name: string; 19 | client_id: string; 20 | client_secret: string; 21 | id: string; 22 | active: boolean; 23 | uuid: string; 24 | enterprise_customer: string; 25 | refresh_token: string; 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLMSTab/LMSConfigs/Cornerstone/CornerstoneTypes.tsx: -------------------------------------------------------------------------------- 1 | export type CornerstoneConfigCamelCase = { 2 | lms: string; 3 | displayName: string; 4 | cornerstoneBaseUrl: string; 5 | id: string; 6 | active: boolean; 7 | uuid: string; 8 | }; 9 | 10 | export type CornerstoneConfigSnakeCase = { 11 | lms: string; 12 | display_name: string; 13 | cornerstone_base_url: string; 14 | id: string; 15 | active: boolean; 16 | uuid: string; 17 | enterprise_customer: string; 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLMSTab/LMSConfigs/Degreed/DegreedTypes.tsx: -------------------------------------------------------------------------------- 1 | export type DegreedConfigCamelCase = { 2 | lms: string; 3 | displayName: string; 4 | clientId: string; 5 | clientSecret: string; 6 | degreedBaseUrl: string; 7 | degreedTokenFetchBaseUrl: string; 8 | id: string; 9 | active: boolean; 10 | uuid: string; 11 | }; 12 | 13 | export type DegreedConfigSnakeCase = { 14 | lms: string; 15 | display_name: string; 16 | client_id: string; 17 | client_secret: string; 18 | degreed_base_url: string; 19 | degreed_token_fetch_base_url: string; 20 | id: string; 21 | active: boolean; 22 | uuid: string; 23 | enterprise_customer: string; 24 | refresh_token: string; 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLMSTab/LMSConfigs/Moodle/MoodleTypes.tsx: -------------------------------------------------------------------------------- 1 | export type MoodleConfigCamelCase = { 2 | lms: string; 3 | displayName: string; 4 | moodleBaseUrl: string; 5 | serviceShortName: string; 6 | token: string; 7 | username: string; 8 | password: string; 9 | id: string; 10 | active: boolean; 11 | uuid: string; 12 | }; 13 | 14 | export type MoodleConfigSnakeCase = { 15 | lms: string; 16 | display_name: string; 17 | moodle_base_url: string; 18 | service_short_name: string; 19 | token: string; 20 | username: string; 21 | password: string; 22 | id: string; 23 | active: boolean; 24 | uuid: string; 25 | enterprise_customer: string; 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLMSTab/LMSConfigs/SAP/SAPTypes.tsx: -------------------------------------------------------------------------------- 1 | export type SAPConfigCamelCase = { 2 | lms: string; 3 | displayName: string; 4 | sapsfBaseUrl: string; 5 | sapsfCompanyId: string; 6 | sapsfUserId: string; 7 | key: string; 8 | secret: string; 9 | userType: string; 10 | id: string; 11 | active: boolean; 12 | uuid: string; 13 | }; 14 | 15 | export type SAPConfigSnakeCase = { 16 | lms: string; 17 | display_name: string; 18 | sapsf_base_url: string; 19 | sapsf_company_id: string; 20 | sapsf_user_id: string; 21 | key: string; 22 | secret: string; 23 | user_type: string; 24 | id: string; 25 | active: boolean; 26 | uuid: string; 27 | enterprise_customer: string; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLMSTab/utils.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash-es'; 2 | 3 | export default function buttonBool(config) { 4 | let returnVal = true; 5 | Object.entries(config).forEach(entry => { 6 | const [key, value] = entry; 7 | // check whether or not the field is an optional value 8 | if ((key !== 'displayName' && key !== 'degreedTokenFetchBaseUrl') && !value) { 9 | returnVal = false; 10 | } 11 | }); 12 | return returnVal; 13 | } 14 | 15 | export const isExistingConfig = (configs, value, existingInput) => { 16 | for (let i = 0; i < configs.length; i++) { 17 | if (configs[i] === value && existingInput === value) { 18 | return true; 19 | } 20 | } 21 | return false; 22 | }; 23 | 24 | export const getStatus = (config) => { 25 | // config.isValid has two arrays of missing and incorrect config fields 26 | // which are required to resolve in order to complete the configuration 27 | if (!isEmpty([...config.isValid[0].missing, ...config.isValid[1].incorrect])) { 28 | return 'Incomplete'; 29 | } 30 | return config.active ? 'Active' : 'Inactive'; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/settings/SettingsSSOTab/testutils.js: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store'; 2 | import thunk from 'redux-thunk'; 3 | 4 | const enterpriseId = 'an-enterprise'; 5 | const initialStore = { 6 | portalConfiguration: { 7 | enterpriseId, 8 | enterpriseSlug: 'sluggy', 9 | enterpriseName: 'sluggyent', 10 | enableLearnerPortal: true, 11 | }, 12 | }; 13 | 14 | const mockStore = configureMockStore([thunk]); 15 | const getMockStore = aStore => mockStore(aStore); 16 | 17 | export { 18 | getMockStore, 19 | initialStore, 20 | enterpriseId, 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/settings/__mocks__/SettingsTabs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useCurrentSettingsTab } from '../data/hooks'; 3 | 4 | const MockSettingsTabs = () => { 5 | const tab = useCurrentSettingsTab(); 6 | 7 | return ( 8 |

{tab}

9 | ); 10 | }; 11 | 12 | export default MockSettingsTabs; 13 | -------------------------------------------------------------------------------- /src/components/settings/utils.js: -------------------------------------------------------------------------------- 1 | import { logError } from '@edx/frontend-platform/logging'; 2 | 3 | export default function handleErrors(error) { 4 | const errorMsg = error.message || error.response?.status <= 300 5 | ? error.message 6 | : JSON.stringify(error.response.data); 7 | logError(errorMsg); 8 | return errorMsg; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/subscriptions/SubscriptionDetailsSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from '@openedx/paragon'; 3 | 4 | const tableRowHeight = 50; 5 | const tableRowCount = 10; 6 | 7 | const SubscriptionDetailsSkeleton = (props) => ( 8 |
9 |
Loading...
10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | ); 22 | 23 | export default SubscriptionDetailsSkeleton; 24 | -------------------------------------------------------------------------------- /src/components/subscriptions/SubscriptionRoutes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | Routes, 6 | Route, 7 | Navigate, 8 | } from 'react-router-dom'; 9 | 10 | import SubscriptionTabs from './SubscriptionTabs'; 11 | import { 12 | DEFAULT_TAB, 13 | SUBSCRIPTIONS_TAB_PARAM, 14 | } from './data/constants'; 15 | import NotFoundPage from '../NotFoundPage'; 16 | 17 | const SubscriptionRoutes = ({ enterpriseSlug }) => ( 18 | 19 | } 22 | /> 23 | } 26 | /> 27 | } /> 28 | 29 | ); 30 | 31 | SubscriptionRoutes.propTypes = { 32 | enterpriseSlug: PropTypes.string.isRequired, 33 | }; 34 | 35 | const mapStateToProps = state => ({ 36 | enterpriseSlug: state.portalConfiguration.enterpriseSlug, 37 | }); 38 | 39 | export default connect(mapStateToProps)(SubscriptionRoutes); 40 | -------------------------------------------------------------------------------- /src/components/subscriptions/buttons/__mocks__/InviteLearnersButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const INVITE_LEARNERS_BUTTON_TEXT = 'Invite learners'; 5 | 6 | const MockInviteLearnersButton = ({ onSuccess, disabled }) => ( 7 | 10 | ); 11 | 12 | MockInviteLearnersButton.propTypes = { 13 | onSuccess: PropTypes.func.isRequired, 14 | disabled: PropTypes.bool, 15 | }; 16 | 17 | MockInviteLearnersButton.defaultProps = { 18 | disabled: false, 19 | }; 20 | export default MockInviteLearnersButton; 21 | -------------------------------------------------------------------------------- /src/components/subscriptions/data/contextHooks.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | // This file exists to avoid a dependency cycle that would be present if this function were in the hooks folder. 3 | import { useContext } from 'react'; 4 | import { SubscriptionContext } from '../SubscriptionData'; 5 | 6 | /* 7 | This hook provides subscription data for an individual enterprise subscription UUID recieved 8 | from the subscriptionUUID param in the route. 9 | */ 10 | export const useSubscriptionFromParams = ({ subscriptionUUID }) => { 11 | // Use UUID to find matching subscription plan in SubscriptionContext, return 404 if not found 12 | const { data: subscriptions, loading } = useContext(SubscriptionContext); 13 | const foundSubscriptionByUUID = Object.values(subscriptions.results).find(sub => sub.uuid === subscriptionUUID); 14 | if (!foundSubscriptionByUUID) { 15 | return [null, loading]; 16 | } 17 | return [foundSubscriptionByUUID, loading]; 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/subscriptions/expiration/SubscriptionExpiration.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { SubscriptionContext } from '../SubscriptionData'; 4 | import SubscriptionDetailContextProvider from '../SubscriptionDetailContextProvider'; 5 | import SubscriptionExpirationBanner from './SubscriptionExpirationBanner'; 6 | import SubscriptionExpirationModals from './SubscriptionExpirationModals'; 7 | 8 | const SubscriptionExpiration = () => { 9 | const { data } = useContext(SubscriptionContext); 10 | const subscriptions = data.results; 11 | 12 | const subscriptionFurthestFromExpiration = subscriptions.reduce((sub1, sub2) => ( 13 | new Date(sub1.expirationDate) > new Date(sub2.expirationDate) ? sub1 : sub2)); 14 | 15 | return ( 16 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default SubscriptionExpiration; 27 | -------------------------------------------------------------------------------- /src/components/subscriptions/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { default as SubscriptionManagementPage } from './SubscriptionManagementPage'; 3 | export { default as MultipleSubscriptionsPage } from './MultipleSubscriptionsPage'; 4 | export { default as SubscriptionData } from './SubscriptionData'; 5 | -------------------------------------------------------------------------------- /src/components/subscriptions/licenses/LicenseAllocationDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LicenseAllocationHeader from './LicenseAllocationHeader'; 3 | import LicenseManagementTable from './LicenseManagementTable'; 4 | 5 | const LicenseAllocationDetails = () => ( 6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 | ); 15 | 16 | export default LicenseAllocationDetails; 17 | -------------------------------------------------------------------------------- /src/components/subscriptions/licenses/LicenseManagementTable/LicenseManagementUserBadge.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Badge, 5 | } from '@openedx/paragon'; 6 | 7 | import { 8 | USER_STATUS_BADGE_MAP, 9 | ACTIVATED, 10 | ASSIGNED, 11 | REVOKED, 12 | } from '../../data/constants'; 13 | 14 | const LicenseManagementUserBadge = ({ userStatus }) => { 15 | const badgeLabel = USER_STATUS_BADGE_MAP[userStatus]; 16 | 17 | if (badgeLabel) { 18 | return {badgeLabel.label}; 19 | } 20 | // If userStatus is undefined return no badge 21 | return null; 22 | }; 23 | 24 | LicenseManagementUserBadge.propTypes = { 25 | userStatus: PropTypes.oneOf([ACTIVATED, ASSIGNED, REVOKED]).isRequired, 26 | }; 27 | 28 | export default LicenseManagementUserBadge; 29 | -------------------------------------------------------------------------------- /src/components/subscriptions/licenses/LicenseManagementTable/tests/LicenseManagementUserBadge.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | screen, 4 | render, 5 | cleanup, 6 | } from '@testing-library/react'; 7 | 8 | import LicenseManagementUserBadge from '../LicenseManagementUserBadge'; 9 | import { 10 | ASSIGNED, 11 | ACTIVATED, 12 | REVOKED, 13 | } from '../../../data/constants'; 14 | 15 | const variants = [ 16 | { userStatus: ACTIVATED, label: 'Active' }, 17 | { userStatus: ASSIGNED, label: 'Pending' }, 18 | { userStatus: REVOKED, label: 'Revoked' }, 19 | ]; 20 | 21 | describe('', () => { 22 | afterEach(() => { 23 | cleanup(); 24 | }); 25 | 26 | test.each(variants)('display right badge for variant %p', (variant) => { 27 | render(); 28 | expect(screen.getByText(variant.label)).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/subscriptions/styles/index.scss: -------------------------------------------------------------------------------- 1 | .manage-subscription { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | 6 | .add-users-dropdown { 7 | .dropdown-menu { 8 | z-index: $zindex-fixed; 9 | } 10 | } 11 | 12 | .subscription-card { 13 | margin-bottom: map_get($spacers, 4); 14 | } 15 | 16 | @include media-breakpoint-up(lg) { 17 | .add-users-dropdown { 18 | float: right; 19 | 20 | .dropdown-menu { 21 | left: inherit; 22 | right: 0; 23 | } 24 | } 25 | } 26 | } 27 | 28 | .pgn__modal-title .enroll-header { 29 | display: inline-block; 30 | background-color: transparent; 31 | color: $red; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/subsidy-request-management-alerts/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as NoAvailableLicensesBanner } from './NoAvailableLicensesBanner'; 2 | export { default as NoAvailableCodesBanner } from './NoAvailableCodesBanner'; 3 | -------------------------------------------------------------------------------- /src/components/subsidy-request-modals/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as ApproveLicenseRequestModal } from './ApproveLicenseRequestModal'; 2 | export { default as ApproveCouponCodeRequestModal } from './ApproveCouponCodeRequestModal'; 3 | export { default as DeclineSubsidyRequestModal } from './DeclineSubsidyRequestModal'; 4 | -------------------------------------------------------------------------------- /src/components/subsidy-requests/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { SubsidyRequestsContext } from './SubsidyRequestsContext'; 3 | -------------------------------------------------------------------------------- /src/components/system-wide-banner/SystemWideWarningBanner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { PageBanner, Icon } from '@openedx/paragon'; 4 | import { WarningFilled } from '@openedx/paragon/icons'; 5 | 6 | const SystemWideWarningBanner = ({ children }) => ( 7 | 8 | 9 | {children} 10 | 11 | ); 12 | 13 | SystemWideWarningBanner.propTypes = { 14 | children: PropTypes.node.isRequired, 15 | }; 16 | 17 | export default SystemWideWarningBanner; 18 | -------------------------------------------------------------------------------- /src/components/system-wide-banner/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export { default as SystemWideWarningBanner } from './SystemWideWarningBanner'; 3 | -------------------------------------------------------------------------------- /src/containers/AdminCards/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import AdminCards from '../../components/Admin/AdminCards'; 4 | 5 | const mapStateToProps = state => ({ 6 | activeLearners: state.dashboardAnalytics.active_learners, 7 | enrolledLearners: state.dashboardAnalytics.enrolled_learners, 8 | numberOfUsers: state.dashboardAnalytics.number_of_users, 9 | courseCompletions: state.dashboardAnalytics.course_completions, 10 | }); 11 | 12 | export default connect(mapStateToProps)(AdminCards); 13 | -------------------------------------------------------------------------------- /src/containers/AdminCardsV2/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import AdminCards from '../../components/AdminV2/AdminCards'; 4 | 5 | const mapStateToProps = state => ({ 6 | activeLearners: state.dashboardAnalytics.active_learners, 7 | enrolledLearners: state.dashboardAnalytics.enrolled_learners, 8 | numberOfUsers: state.dashboardAnalytics.number_of_users, 9 | courseCompletions: state.dashboardAnalytics.course_completions, 10 | }); 11 | 12 | export default connect(mapStateToProps)(AdminCards); 13 | -------------------------------------------------------------------------------- /src/containers/CouponDetails/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import CouponDetails from '../../components/CouponDetails'; 4 | 5 | import { fetchCouponOrder } from '../../data/actions/coupons'; 6 | 7 | const couponDetailsTableId = 'coupon-details'; 8 | 9 | const mapStateToProps = state => ({ 10 | couponDetailsTable: state.table[couponDetailsTableId], 11 | couponOverviewError: state.coupons.couponOverviewError, 12 | couponOverviewLoading: state.coupons.couponOverviewLoading, 13 | }); 14 | 15 | const mapDispatchToProps = dispatch => ({ 16 | fetchCouponOrder: (couponId) => { 17 | dispatch(fetchCouponOrder(couponId)); 18 | }, 19 | }); 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps, 24 | )(CouponDetails); 25 | -------------------------------------------------------------------------------- /src/containers/DownloadCsvButton/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { fetchCsv, clearCsv } from '../../data/actions/csv'; 4 | import DownloadCsvButton from '../../components/DownloadCsvButton'; 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | const csvState = state.csv[ownProps.id] || {}; 8 | return { 9 | enterpriseId: state.portalConfiguration.enterpriseId, 10 | csvLoading: csvState.csvLoading, 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch, ownProps) => ({ 15 | fetchCsv: (fetchMethod) => { 16 | dispatch(fetchCsv(ownProps.id, fetchMethod)); 17 | }, 18 | clearCsv: () => { 19 | dispatch(clearCsv(ownProps.id)); 20 | }, 21 | }); 22 | 23 | export default connect(mapStateToProps, mapDispatchToProps)(DownloadCsvButton); 24 | -------------------------------------------------------------------------------- /src/containers/EnterpriseIndexPage/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import EnterpriseList from '../../components/EnterpriseList'; 3 | import { clearPortalConfiguration } from '../../data/actions/portalConfiguration'; 4 | 5 | const mapDispatchToProps = dispatch => ({ 6 | clearPortalConfiguration: () => { 7 | dispatch(clearPortalConfiguration()); 8 | }, 9 | }); 10 | 11 | const EnterpriseIndexPage = connect( 12 | null, 13 | mapDispatchToProps, 14 | )(EnterpriseList); 15 | 16 | export default EnterpriseIndexPage; 17 | -------------------------------------------------------------------------------- /src/containers/Footer/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Footer from '../../components/Footer'; 4 | 5 | const mapStateToProps = state => ({ 6 | enterpriseName: state.portalConfiguration.enterpriseName, 7 | enterpriseSlug: state.portalConfiguration.enterpriseSlug, 8 | enterpriseLogo: state.portalConfiguration.enterpriseBranding?.logo, 9 | }); 10 | 11 | export default connect(mapStateToProps)(Footer); 12 | -------------------------------------------------------------------------------- /src/containers/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Header from '../../components/Header'; 4 | 5 | const mapStateToProps = (state) => ({ 6 | enterpriseName: state.portalConfiguration.enterpriseName, 7 | enterpriseSlug: state.portalConfiguration.enterpriseSlug, 8 | enterpriseLogo: state.portalConfiguration.enterpriseBranding?.logo, 9 | hasSidebarToggle: state.sidebar.hasSidebarToggle, 10 | }); 11 | 12 | export default connect(mapStateToProps)(Header); 13 | -------------------------------------------------------------------------------- /src/containers/InviteLearnersModal/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import InviteLearnersModal from '../../components/InviteLearnersModal'; 4 | 5 | import addLicensesForUsers from '../../data/actions/userSubscription'; 6 | 7 | const mapStateToProps = state => ({ 8 | contactEmail: state.portalConfiguration.contactEmail, 9 | }); 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | addLicensesForUsers: (options, subscriptionUUID) => new Promise((resolve, reject) => { 13 | dispatch(addLicensesForUsers({ 14 | options, 15 | subscriptionUUID, 16 | onSuccess: (response) => { resolve(response); }, 17 | onError: (error) => { reject(error); }, 18 | })); 19 | }), 20 | }); 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(InviteLearnersModal); 23 | -------------------------------------------------------------------------------- /src/containers/SaveTemplateButton/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { saveTemplate } from '../../data/actions/emailTemplate'; 4 | import SaveTemplateButton from '../../components/SaveTemplateButton'; 5 | 6 | const mapStateToProps = state => ({ 7 | saving: state.emailTemplate.saving, 8 | emailTemplateSource: state.emailTemplate.emailTemplateSource, 9 | emailTemplates: state.emailTemplate, 10 | }); 11 | 12 | const mapDispatchToProps = dispatch => ({ 13 | saveTemplate: options => new Promise((resolve, reject) => { 14 | dispatch(saveTemplate({ 15 | options, 16 | onSuccess: (response) => { resolve(response); }, 17 | onError: (error) => { reject(error); }, 18 | })); 19 | }), 20 | }); 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(SaveTemplateButton); 23 | -------------------------------------------------------------------------------- /src/containers/SidebarToggle/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import SidebarToggle from '../../components/SidebarToggle'; 4 | 5 | import { 6 | expandSidebar, 7 | collapseSidebar, 8 | } from '../../data/actions/sidebar'; 9 | 10 | const mapStateToProps = state => ({ 11 | isExpandedByToggle: state.sidebar.isExpandedByToggle, 12 | }); 13 | 14 | const mapDispatchToProps = dispatch => ({ 15 | expandSidebar: () => dispatch(expandSidebar(true)), 16 | collapseSidebar: () => dispatch(collapseSidebar(true)), 17 | }); 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(SidebarToggle); 20 | -------------------------------------------------------------------------------- /src/containers/TemplateSourceFields/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import TemplateSourceFields from '../../components/TemplateSourceFields'; 4 | 5 | import fetchEmailTemplates, { setEmailTemplateSource, currentFromTemplate, setEmailAddress } from '../../data/actions/emailTemplate'; 6 | 7 | const mapStateToProps = state => ({ 8 | emailTemplateSource: state.emailTemplate.emailTemplateSource, 9 | allEmailTemplates: state.emailTemplate.allTemplates, 10 | }); 11 | 12 | const mapDispatchToProps = dispatch => ({ 13 | setEmailTemplateSource: templateSource => dispatch(setEmailTemplateSource(templateSource)), 14 | setEmailAddress: (emailAddress, emailType) => dispatch(setEmailAddress(emailAddress, emailType)), 15 | currentFromTemplate: (type, template) => dispatch(currentFromTemplate(type, template)), 16 | fetchEmailTemplates: (options) => { 17 | dispatch(fetchEmailTemplates(options)); 18 | }, 19 | }); 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(TemplateSourceFields); 22 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React = require('react'); 3 | 4 | export const ReactComponent: React.FC>; 5 | const src: string; 6 | export default src; 7 | } 8 | -------------------------------------------------------------------------------- /src/data/actions/sidebar.js: -------------------------------------------------------------------------------- 1 | import { 2 | EXPAND_SIDEBAR, 3 | COLLAPSE_SIDEBAR, 4 | TOGGLE_SIDEBAR_TOGGLE, 5 | } from '../constants/sidebar'; 6 | 7 | const expandSidebar = (usingToggle = false) => ({ 8 | type: EXPAND_SIDEBAR, 9 | payload: { 10 | usingToggle, 11 | }, 12 | }); 13 | 14 | const collapseSidebar = (usingToggle = false) => ({ 15 | type: COLLAPSE_SIDEBAR, 16 | payload: { 17 | usingToggle, 18 | }, 19 | }); 20 | 21 | const toggleSidebarToggle = () => ({ 22 | type: TOGGLE_SIDEBAR_TOGGLE, 23 | }); 24 | 25 | export { 26 | expandSidebar, 27 | collapseSidebar, 28 | toggleSidebarToggle, 29 | }; 30 | -------------------------------------------------------------------------------- /src/data/constants/addUsers.js: -------------------------------------------------------------------------------- 1 | const EMAIL_ADDRESS_TEXT_FORM_DATA = 'email-addresses'; 2 | const EMAIL_ADDRESS_CSV_FORM_DATA = 'csv-email-addresses'; 3 | const NOTIFY_LEARNERS_FORM_DATA = 'notify'; 4 | const NOTIFY_LEARNERS_LABEL = 'Notify learners'; 5 | const EMAIL_ADDRESS_CSV_LABEL = 'Upload email addresses'; 6 | const EMAIL_ADDRESS_TEXT_LABEL = 'Email addresses'; 7 | const MAX_EMAIL_ADDRESS_ALLOWED = 500; 8 | 9 | export { 10 | MAX_EMAIL_ADDRESS_ALLOWED, 11 | EMAIL_ADDRESS_TEXT_FORM_DATA, 12 | EMAIL_ADDRESS_CSV_FORM_DATA, 13 | NOTIFY_LEARNERS_FORM_DATA, 14 | NOTIFY_LEARNERS_LABEL, 15 | EMAIL_ADDRESS_CSV_LABEL, 16 | EMAIL_ADDRESS_TEXT_LABEL, 17 | }; 18 | -------------------------------------------------------------------------------- /src/data/constants/codeAssignment.js: -------------------------------------------------------------------------------- 1 | const CODE_ASSIGNMENT_REQUEST = 'CODE_ASSIGNMENT_REQUEST'; 2 | const CODE_ASSIGNMENT_SUCCESS = 'CODE_ASSIGNMENT_SUCCESS'; 3 | const CODE_ASSIGNMENT_FAILURE = 'CODE_ASSIGNMENT_FAILURE'; 4 | 5 | export { 6 | CODE_ASSIGNMENT_REQUEST, 7 | CODE_ASSIGNMENT_SUCCESS, 8 | CODE_ASSIGNMENT_FAILURE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/data/constants/codeReminder.js: -------------------------------------------------------------------------------- 1 | const CODE_REMINDER_REQUEST = 'CODE_REMINDER_REQUEST'; 2 | const CODE_REMINDER_SUCCESS = 'CODE_REMINDER_SUCCESS'; 3 | const CODE_REMINDER_FAILURE = 'CODE_REMINDER_FAILURE'; 4 | 5 | export { 6 | CODE_REMINDER_REQUEST, 7 | CODE_REMINDER_SUCCESS, 8 | CODE_REMINDER_FAILURE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/data/constants/codeRevoke.js: -------------------------------------------------------------------------------- 1 | const CODE_REVOKE_REQUEST = 'CODE_REVOKE_REQUEST'; 2 | const CODE_REVOKE_SUCCESS = 'CODE_REVOKE_SUCCESS'; 3 | const CODE_REVOKE_FAILURE = 'CODE_REVOKE_FAILURE'; 4 | 5 | export { 6 | CODE_REVOKE_REQUEST, 7 | CODE_REVOKE_SUCCESS, 8 | CODE_REVOKE_FAILURE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/data/constants/coupons.js: -------------------------------------------------------------------------------- 1 | const COUPONS_REQUEST = 'COUPONS_REQUEST'; 2 | const COUPONS_SUCCESS = 'COUPONS_SUCCESS'; 3 | const COUPONS_FAILURE = 'COUPONS_FAILURE'; 4 | const CLEAR_COUPONS = 'CLEAR_COUPONS'; 5 | const COUPON_REQUEST = 'COUPON_REQUEST'; 6 | const COUPON_SUCCESS = 'COUPON_SUCCESS'; 7 | const COUPON_FAILURE = 'COUPON_FAILURE'; 8 | 9 | // Coupon types 10 | const SINGLE_USE = 'Single use'; 11 | const MULTI_USE = 'Multi-use'; 12 | const ONCE_PER_CUSTOMER = 'Once per customer'; 13 | const MULTI_USE_PER_CUSTOMER = 'Multi-use-per-Customer'; 14 | const CSV_HEADER_NAME = 'emails'; 15 | 16 | export { 17 | // Redux action names 18 | COUPONS_REQUEST, 19 | COUPONS_SUCCESS, 20 | COUPONS_FAILURE, 21 | CLEAR_COUPONS, 22 | COUPON_REQUEST, 23 | COUPON_SUCCESS, 24 | COUPON_FAILURE, 25 | 26 | // Coupon types 27 | SINGLE_USE, 28 | ONCE_PER_CUSTOMER, 29 | MULTI_USE, 30 | MULTI_USE_PER_CUSTOMER, 31 | 32 | // column name in emails csv file 33 | CSV_HEADER_NAME, 34 | }; 35 | -------------------------------------------------------------------------------- /src/data/constants/createPendingEntUser.js: -------------------------------------------------------------------------------- 1 | const PENDING_ENT_USER_REQUEST = 'PENDING_ENT_USER_REQUEST'; 2 | const PENDING_ENT_USER_SUCCESS = 'PENDING_ENT_USER_SUCCESS'; 3 | const PENDING_ENT_USER_FAILURE = 'PENDING_ENT_USER_FAILURE'; 4 | 5 | export { 6 | PENDING_ENT_USER_REQUEST, 7 | PENDING_ENT_USER_SUCCESS, 8 | PENDING_ENT_USER_FAILURE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/data/constants/csv.js: -------------------------------------------------------------------------------- 1 | const FETCH_CSV_REQUEST = 'FETCH_CSV_REQUEST'; 2 | const FETCH_CSV_SUCCESS = 'FETCH_CSV_SUCCESS'; 3 | const FETCH_CSV_FAILURE = 'FETCH_CSV_FAILURE'; 4 | const CLEAR_CSV = 'CLEAR_CSV'; 5 | 6 | export { 7 | FETCH_CSV_REQUEST, 8 | FETCH_CSV_SUCCESS, 9 | FETCH_CSV_FAILURE, 10 | CLEAR_CSV, 11 | }; 12 | -------------------------------------------------------------------------------- /src/data/constants/dashboardAnalytics.js: -------------------------------------------------------------------------------- 1 | const FETCH_DASHBOARD_ANALYTICS_REQUEST = 'FETCH_DASHBOARD_ANALYTICS_REQUEST'; 2 | const FETCH_DASHBOARD_ANALYTICS_SUCCESS = 'FETCH_DASHBOARD_ANALYTICS_SUCCESS'; 3 | const FETCH_DASHBOARD_ANALYTICS_FAILURE = 'FETCH_DASHBOARD_ANALYTICS_FAILURE'; 4 | const CLEAR_DASHBOARD_ANALYTICS = 'CLEAR_DASHBOARD_ANALYTICS'; 5 | 6 | export { 7 | FETCH_DASHBOARD_ANALYTICS_REQUEST, 8 | FETCH_DASHBOARD_ANALYTICS_SUCCESS, 9 | FETCH_DASHBOARD_ANALYTICS_FAILURE, 10 | CLEAR_DASHBOARD_ANALYTICS, 11 | }; 12 | -------------------------------------------------------------------------------- /src/data/constants/dashboardInsights.js: -------------------------------------------------------------------------------- 1 | const FETCH_DASHBOARD_INSIGHTS_REQUEST = 'FETCH_DASHBOARD_INSIGHTS_REQUEST'; 2 | const FETCH_DASHBOARD_INSIGHTS_SUCCESS = 'FETCH_DASHBOARD_INSIGHTS_SUCCESS'; 3 | const FETCH_DASHBOARD_INSIGHTS_FAILURE = 'FETCH_DASHBOARD_INSIGHTS_FAILURE'; 4 | const CLEAR_DASHBOARD_INSIGHTS = 'CLEAR_DASHBOARD_INSIGHTS'; 5 | 6 | export { 7 | FETCH_DASHBOARD_INSIGHTS_REQUEST, 8 | FETCH_DASHBOARD_INSIGHTS_SUCCESS, 9 | FETCH_DASHBOARD_INSIGHTS_FAILURE, 10 | CLEAR_DASHBOARD_INSIGHTS, 11 | }; 12 | -------------------------------------------------------------------------------- /src/data/constants/enterpriseBudgets.js: -------------------------------------------------------------------------------- 1 | const FETCH_ENTERPRISE_BUDGETS_REQUEST = 'FETCH_ENTERPRISE_BUDGETS_REQUEST'; 2 | const FETCH_ENTERPRISE_BUDGETS_SUCCESS = 'FETCH_ENTERPRISE_BUDGETS_SUCCESS'; 3 | const FETCH_ENTERPRISE_BUDGETS_FAILURE = 'FETCH_ENTERPRISE_BUDGETS_FAILURE'; 4 | const CLEAR_ENTERPRISE_BUDGETS = 'CLEAR_ENTERPRISE_BUDGETS'; 5 | 6 | export { 7 | FETCH_ENTERPRISE_BUDGETS_REQUEST, 8 | FETCH_ENTERPRISE_BUDGETS_SUCCESS, 9 | FETCH_ENTERPRISE_BUDGETS_FAILURE, 10 | CLEAR_ENTERPRISE_BUDGETS, 11 | }; 12 | -------------------------------------------------------------------------------- /src/data/constants/enterpriseCustomerAdmin.ts: -------------------------------------------------------------------------------- 1 | const FETCH_ENTERPRISE_CUSTOMER_ADMIN_REQUEST = 'FETCH_ENTERPRISE_CUSTOMER_ADMIN_REQUEST'; 2 | const FETCH_ENTERPRISE_CUSTOMER_ADMIN_SUCCESS = 'FETCH_ENTERPRISE_CUSTOMER_ADMIN_SUCCESS'; 3 | const FETCH_ENTERPRISE_CUSTOMER_ADMIN_FAILURE = 'FETCH_ENTERPRISE_CUSTOMER_ADMIN_FAILURE'; 4 | const UPDATE_ENTERPRISE_CUSTOMER_ADMIN = 'UPDATE_ENTERPRISE_CUSTOMER_ADMIN'; 5 | const DISMISS_ONBOARDING_TOUR_SUCCESS = 'DISMISS_ONBOARDING_TOUR_SUCCESS'; 6 | const DISMISS_ONBOARDING_TOUR_FAILURE = 'DISMISS_ONBOARDING_TOUR_FAILURE'; 7 | const SET_ONBOARDING_TOUR_DISMISSED = 'SET_ONBOARDING_TOUR_DISMISSED'; 8 | 9 | export { 10 | FETCH_ENTERPRISE_CUSTOMER_ADMIN_REQUEST, 11 | FETCH_ENTERPRISE_CUSTOMER_ADMIN_SUCCESS, 12 | FETCH_ENTERPRISE_CUSTOMER_ADMIN_FAILURE, 13 | UPDATE_ENTERPRISE_CUSTOMER_ADMIN, 14 | DISMISS_ONBOARDING_TOUR_SUCCESS, 15 | DISMISS_ONBOARDING_TOUR_FAILURE, 16 | SET_ONBOARDING_TOUR_DISMISSED, 17 | }; 18 | -------------------------------------------------------------------------------- /src/data/constants/enterpriseGroups.js: -------------------------------------------------------------------------------- 1 | const FETCH_ENTERPRISE_GROUPS_REQUEST = 'FETCH_ENTERPRISE_GROUPS_REQUEST'; 2 | const FETCH_ENTERPRISE_GROUPS_SUCCESS = 'FETCH_ENTERPRISE_GROUPS_SUCCESS'; 3 | const FETCH_ENTERPRISE_GROUPS_FAILURE = 'FETCH_ENTERPRISE_GROUPS_FAILURE'; 4 | const CLEAR_ENTERPRISE_GROUPS = 'CLEAR_ENTERPRISE_GROUPS'; 5 | 6 | export { 7 | FETCH_ENTERPRISE_GROUPS_REQUEST, 8 | FETCH_ENTERPRISE_GROUPS_SUCCESS, 9 | FETCH_ENTERPRISE_GROUPS_FAILURE, 10 | CLEAR_ENTERPRISE_GROUPS, 11 | }; 12 | -------------------------------------------------------------------------------- /src/data/constants/formSubmissions.js: -------------------------------------------------------------------------------- 1 | const SUBMIT_STATES = { 2 | ERROR: 'error', 3 | DEFAULT: 'default', 4 | COMPLETE: 'complete', 5 | PENDING: 'pending', 6 | }; 7 | 8 | export default SUBMIT_STATES; 9 | -------------------------------------------------------------------------------- /src/data/constants/licenseReminder.js: -------------------------------------------------------------------------------- 1 | export const LICENSE_REMIND_REQUEST = 'LICENSE_REMIND_REQUEST'; 2 | export const LICENSE_REMIND_SUCCESS = 'LICENSE_REMIND_SUCCESS'; 3 | export const LICENSE_REMIND_FAILURE = 'LICENSE_REMIND_FAILURE'; 4 | -------------------------------------------------------------------------------- /src/data/constants/licenseRevoke.js: -------------------------------------------------------------------------------- 1 | export const LICENSE_REVOKE_REQUEST = 'LICENSE_REVOKE_REQUEST'; 2 | export const LICENSE_REVOKE_SUCCESS = 'LICENSE_REVOKE_SUCCESS'; 3 | export const LICENSE_REVOKE_FAILURE = 'LICENSE_REVOKE_FAILURE'; 4 | -------------------------------------------------------------------------------- /src/data/constants/portalConfiguration.js: -------------------------------------------------------------------------------- 1 | const FETCH_PORTAL_CONFIGURATION_REQUEST = 'FETCH_PORTAL_CONFIGURATION_REQUEST'; 2 | const FETCH_PORTAL_CONFIGURATION_SUCCESS = 'FETCH_PORTAL_CONFIGURATION_SUCCESS'; 3 | const FETCH_PORTAL_CONFIGURATION_FAILURE = 'FETCH_PORTAL_CONFIGURATION_FAILURE'; 4 | const CLEAR_PORTAL_CONFIGURATION = 'CLEAR_PORTAL_CONFIGURATION'; 5 | const UPDATE_PORTAL_CONFIGURATION = 'UPDATE_PORTAL_CONFIGURATION'; 6 | 7 | export { 8 | FETCH_PORTAL_CONFIGURATION_REQUEST, 9 | FETCH_PORTAL_CONFIGURATION_SUCCESS, 10 | FETCH_PORTAL_CONFIGURATION_FAILURE, 11 | CLEAR_PORTAL_CONFIGURATION, 12 | UPDATE_PORTAL_CONFIGURATION, 13 | }; 14 | -------------------------------------------------------------------------------- /src/data/constants/sidebar.js: -------------------------------------------------------------------------------- 1 | const EXPAND_SIDEBAR = 'EXPAND_SIDEBAR'; 2 | const COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR'; 3 | const TOGGLE_SIDEBAR_TOGGLE = 'TOGGLE_SIDEBAR_TOGGLE'; 4 | 5 | export { 6 | EXPAND_SIDEBAR, 7 | COLLAPSE_SIDEBAR, 8 | TOGGLE_SIDEBAR_TOGGLE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/data/constants/subsidyRequests.js: -------------------------------------------------------------------------------- 1 | export const SUPPORTED_SUBSIDY_TYPES = { 2 | coupon: 'coupon', 3 | license: 'license', 4 | }; 5 | 6 | export const SUBSIDY_REQUEST_STATUS = { 7 | REQUESTED: 'requested', 8 | PENDING: 'pending', 9 | APPROVED: 'approved', 10 | DECLINED: 'declined', 11 | ERROR: 'error', 12 | }; 13 | -------------------------------------------------------------------------------- /src/data/constants/subsidyTypes.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const SUBSIDY_TYPES = { 3 | coupon: 'coupon', 4 | license: 'license', 5 | budget: 'budget', 6 | }; 7 | -------------------------------------------------------------------------------- /src/data/constants/table.js: -------------------------------------------------------------------------------- 1 | const PAGINATION_REQUEST = 'PAGINATION_REQUEST'; 2 | const PAGINATION_SUCCESS = 'PAGINATION_SUCCESS'; 3 | const PAGINATION_FAILURE = 'PAGINATION_FAILURE'; 4 | const SORT_REQUEST = 'SORT_REQUEST'; 5 | const SORT_SUCCESS = 'SORT_SUCCESS'; 6 | const SORT_FAILURE = 'SORT_FAILURE'; 7 | const CLEAR_TABLE = 'CLEAR_TABLE'; 8 | 9 | export { 10 | PAGINATION_REQUEST, 11 | PAGINATION_SUCCESS, 12 | PAGINATION_FAILURE, 13 | SORT_REQUEST, 14 | SORT_SUCCESS, 15 | SORT_FAILURE, 16 | CLEAR_TABLE, 17 | }; 18 | -------------------------------------------------------------------------------- /src/data/constants/userSubscription.js: -------------------------------------------------------------------------------- 1 | const USER_SUBSCRIPTION_REQUEST = 'USER_SUBSCRIPTION_REQUEST'; 2 | const USER_SUBSCRIPTION_SUCCESS = 'USER_SUBSCRIPTION_SUCCESS'; 3 | const USER_SUBSCRIPTION_FAILURE = 'USER_SUBSCRIPTION_FAILURE'; 4 | 5 | export { 6 | USER_SUBSCRIPTION_REQUEST, 7 | USER_SUBSCRIPTION_SUCCESS, 8 | USER_SUBSCRIPTION_FAILURE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/data/reducers/codeAssignment.js: -------------------------------------------------------------------------------- 1 | import { 2 | CODE_ASSIGNMENT_REQUEST, 3 | CODE_ASSIGNMENT_SUCCESS, 4 | CODE_ASSIGNMENT_FAILURE, 5 | } from '../constants/codeAssignment'; 6 | 7 | const initialState = { 8 | loading: false, 9 | error: null, 10 | data: null, 11 | }; 12 | 13 | const codeAssignment = (state = initialState, action) => { 14 | switch (action.type) { 15 | case CODE_ASSIGNMENT_REQUEST: 16 | return { 17 | loading: true, 18 | error: null, 19 | }; 20 | case CODE_ASSIGNMENT_SUCCESS: 21 | return { 22 | loading: false, 23 | error: null, 24 | data: action.payload.data, 25 | }; 26 | case CODE_ASSIGNMENT_FAILURE: 27 | return { 28 | loading: false, 29 | error: action.payload.error, 30 | }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | 36 | export default codeAssignment; 37 | -------------------------------------------------------------------------------- /src/data/reducers/codeRevoke.js: -------------------------------------------------------------------------------- 1 | import { 2 | CODE_REVOKE_REQUEST, 3 | CODE_REVOKE_SUCCESS, 4 | CODE_REVOKE_FAILURE, 5 | } from '../constants/codeRevoke'; 6 | 7 | const initialState = { 8 | loading: false, 9 | error: null, 10 | data: null, 11 | }; 12 | 13 | const codeRevoke = (state = initialState, action) => { 14 | switch (action.type) { 15 | case CODE_REVOKE_REQUEST: 16 | return { 17 | loading: true, 18 | error: null, 19 | }; 20 | case CODE_REVOKE_SUCCESS: 21 | return { 22 | loading: false, 23 | error: null, 24 | data: action.payload.data, 25 | }; 26 | case CODE_REVOKE_FAILURE: 27 | return { 28 | loading: false, 29 | error: action.payload.error, 30 | }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | 36 | export default codeRevoke; 37 | -------------------------------------------------------------------------------- /src/data/reducers/licenseRemind.js: -------------------------------------------------------------------------------- 1 | import { 2 | LICENSE_REMIND_REQUEST, 3 | LICENSE_REMIND_SUCCESS, 4 | LICENSE_REMIND_FAILURE, 5 | } from '../constants/licenseReminder'; 6 | 7 | const initialState = { 8 | loading: false, 9 | error: null, 10 | data: null, 11 | }; 12 | 13 | const licenseRemind = (state = initialState, action) => { 14 | switch (action.type) { 15 | case LICENSE_REMIND_REQUEST: 16 | return { 17 | loading: true, 18 | error: null, 19 | }; 20 | case LICENSE_REMIND_SUCCESS: 21 | return { 22 | loading: false, 23 | error: null, 24 | data: action.payload.data, 25 | }; 26 | case LICENSE_REMIND_FAILURE: 27 | return { 28 | loading: false, 29 | error: action.payload.error, 30 | }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | 36 | export default licenseRemind; 37 | -------------------------------------------------------------------------------- /src/data/reducers/licenseRevoke.js: -------------------------------------------------------------------------------- 1 | import { 2 | LICENSE_REVOKE_REQUEST, 3 | LICENSE_REVOKE_SUCCESS, 4 | LICENSE_REVOKE_FAILURE, 5 | } from '../constants/licenseRevoke'; 6 | 7 | const initialState = { 8 | loading: false, 9 | error: null, 10 | data: null, 11 | }; 12 | 13 | const licenseRevoke = (state = initialState, action) => { 14 | switch (action.type) { 15 | case LICENSE_REVOKE_REQUEST: 16 | return { 17 | ...state, 18 | loading: true, 19 | error: null, 20 | }; 21 | case LICENSE_REVOKE_SUCCESS: 22 | return { 23 | ...state, 24 | loading: false, 25 | error: null, 26 | data: action.payload.data, 27 | }; 28 | case LICENSE_REVOKE_FAILURE: 29 | return { 30 | ...state, 31 | loading: false, 32 | error: action.payload.error, 33 | }; 34 | default: 35 | return state; 36 | } 37 | }; 38 | 39 | export default licenseRevoke; 40 | -------------------------------------------------------------------------------- /src/data/reducers/sidebar.js: -------------------------------------------------------------------------------- 1 | import { 2 | EXPAND_SIDEBAR, 3 | COLLAPSE_SIDEBAR, 4 | TOGGLE_SIDEBAR_TOGGLE, 5 | } from '../constants/sidebar'; 6 | 7 | const initialState = { 8 | isExpanded: false, 9 | isExpandedByToggle: false, 10 | hasSidebarToggle: false, 11 | }; 12 | 13 | const sidebarReducer = (state = initialState, action) => { 14 | const getStateKey = () => { 15 | const { payload: { usingToggle } } = action; 16 | return usingToggle ? 'isExpandedByToggle' : 'isExpanded'; 17 | }; 18 | 19 | switch (action.type) { 20 | case EXPAND_SIDEBAR: 21 | return { 22 | ...state, 23 | [getStateKey()]: true, 24 | }; 25 | case COLLAPSE_SIDEBAR: 26 | return { 27 | ...state, 28 | [getStateKey()]: false, 29 | }; 30 | case TOGGLE_SIDEBAR_TOGGLE: 31 | return { 32 | ...state, 33 | hasSidebarToggle: !state.hasSidebarToggle, 34 | }; 35 | default: 36 | return state; 37 | } 38 | }; 39 | 40 | export default sidebarReducer; 41 | -------------------------------------------------------------------------------- /src/data/reducers/userSubscription.js: -------------------------------------------------------------------------------- 1 | import { 2 | USER_SUBSCRIPTION_REQUEST, 3 | USER_SUBSCRIPTION_SUCCESS, 4 | USER_SUBSCRIPTION_FAILURE, 5 | } from '../constants/userSubscription'; 6 | 7 | const initialState = { 8 | loading: false, 9 | error: null, 10 | data: null, 11 | }; 12 | 13 | const userSubscription = (state = initialState, action) => { 14 | switch (action.type) { 15 | case USER_SUBSCRIPTION_REQUEST: 16 | return { 17 | loading: true, 18 | error: null, 19 | }; 20 | case USER_SUBSCRIPTION_SUCCESS: 21 | return { 22 | loading: false, 23 | error: null, 24 | data: action.payload.data, 25 | }; 26 | case USER_SUBSCRIPTION_FAILURE: 27 | return { 28 | loading: false, 29 | error: action.payload.error, 30 | }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | 36 | export default userSubscription; 37 | -------------------------------------------------------------------------------- /src/data/services/DiscoveryApiService.js: -------------------------------------------------------------------------------- 1 | import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; 2 | 3 | import { configuration } from '../../config'; 4 | 5 | class DiscoveryApiService { 6 | static discoveryBaseUrl = `${configuration.DISCOVERY_BASE_URL}/api/v1`; 7 | 8 | static apiClient = getAuthenticatedHttpClient; 9 | 10 | static fetchCourseDetails(courseKey) { 11 | const url = `${DiscoveryApiService.discoveryBaseUrl}/courses/${courseKey}/`; 12 | return DiscoveryApiService.apiClient().get(url); 13 | } 14 | } 15 | 16 | export default DiscoveryApiService; 17 | -------------------------------------------------------------------------------- /src/data/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction'; 4 | import { createLogger } from 'redux-logger'; 5 | 6 | import reducers from './reducers'; 7 | 8 | const loggerMiddleware = createLogger(); 9 | 10 | const middleware = [thunkMiddleware, loggerMiddleware]; 11 | 12 | const store = createStore( 13 | reducers, 14 | {}, 15 | composeWithDevTools(applyMiddleware(...middleware)), 16 | ); 17 | 18 | export default store; 19 | -------------------------------------------------------------------------------- /src/hoc.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useParams, useLocation, useNavigate } from 'react-router-dom'; 4 | 5 | export const withParams = WrappedComponent => { 6 | const WithParamsComponent = props => ; 7 | return WithParamsComponent; 8 | }; 9 | 10 | export const withLocation = Component => { 11 | const WrappedComponent = props => { 12 | const location = useLocation(); 13 | return ; 14 | }; 15 | return WrappedComponent; 16 | }; 17 | 18 | export const withNavigate = Component => { 19 | const WrappedComponent = props => { 20 | const navigate = useNavigate(); 21 | return ; 22 | }; 23 | return WrappedComponent; 24 | }; 25 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useInterval } from './useInterval'; 2 | export { default as useOnMount } from './useOnMount'; 3 | -------------------------------------------------------------------------------- /src/hooks/tests/useOnMount.test.js: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks/dom'; 2 | import useOnMount from '../useOnMount'; 3 | 4 | describe('useOnMount', () => { 5 | it('should invoke callback once', () => { 6 | const mockCallback = jest.fn(); 7 | const hook = renderHook(() => useOnMount(mockCallback)); 8 | 9 | const newMockCallback = jest.fn(); 10 | hook.rerender(newMockCallback); 11 | expect(newMockCallback).not.toHaveBeenCalled(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/hooks/useInterval.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const useInterval = (callback, delay) => { 4 | const savedCallback = useRef(); 5 | // Remember the latest callback. 6 | useEffect(() => { 7 | savedCallback.current = callback; 8 | }, [callback]); 9 | 10 | // Set up the interval. 11 | // eslint-disable-next-line consistent-return 12 | useEffect(() => { 13 | function tick() { 14 | savedCallback.current(); 15 | } 16 | if (delay !== null && delay !== undefined && delay > 0) { 17 | const id = setInterval(tick, delay); 18 | return () => clearInterval(id); 19 | } 20 | }, [delay]); 21 | }; 22 | 23 | export default useInterval; 24 | -------------------------------------------------------------------------------- /src/hooks/useOnMount.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | /** 4 | * Invokes the given callback function once when the caller is mounted. 5 | * This hook mimics the behavior of componentDidMount(). 6 | * @param {*} callback function to be invoked 7 | */ 8 | const useOnMount = (callback) => { 9 | const initialMountRef = useRef(false); 10 | 11 | useEffect(() => { 12 | if (!initialMountRef.current) { 13 | initialMountRef.current = true; 14 | callback(); 15 | } 16 | }, [callback]); 17 | }; 18 | 19 | export default useOnMount; 20 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /src/icons/CSOD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/frontend-app-admin-portal/102bd537a81d5582d42206e066e9d36d2a9ab0a0/src/icons/CSOD.png -------------------------------------------------------------------------------- /src/icons/Degreed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/frontend-app-admin-portal/102bd537a81d5582d42206e066e9d36d2a9ab0a0/src/icons/Degreed.png -------------------------------------------------------------------------------- /src/icons/Moodle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/frontend-app-admin-portal/102bd537a81d5582d42206e066e9d36d2a9ab0a0/src/icons/Moodle.png -------------------------------------------------------------------------------- /src/jestGlobalSetup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | process.env.TZ = 'UTC'; 3 | }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@edx/typescript-config", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "types.d.ts", 9 | "src/**/*", 10 | "src/custom.d.ts", 11 | "__mocks__/**/*" 12 | ], 13 | "exclude": ["dist", "node_modules", "src/icons/*"], 14 | } 15 | -------------------------------------------------------------------------------- /webpack.dev-stage.config.js: -------------------------------------------------------------------------------- 1 | const { createConfig } = require('@openedx/frontend-build'); 2 | const path = require('path'); 3 | const dotenv = require('dotenv'); 4 | 5 | /** 6 | * Injects stage-specific env vars from .env.development-stage. 7 | * 8 | * Note: ideally, we could use the base config for `webpack-dev-stage` in 9 | * `getBaseConfig` above, however it appears to have a bug so we have to 10 | * manually load the stage-specific env vars ourselves for now. 11 | * 12 | * The .env.development-stage env vars must be loaded before the base 13 | * config is created. 14 | */ 15 | dotenv.config({ 16 | path: path.resolve(process.cwd(), '.env.development-stage'), 17 | }); 18 | 19 | const config = createConfig('webpack-dev', { 20 | devServer: { 21 | allowedHosts: 'all', 22 | server: 'https', 23 | }, 24 | }); 25 | 26 | config.module.rules[0].exclude = /node_modules\/(?!(lodash-es|@(open)?edx))/; 27 | 28 | module.exports = config; 29 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const {createConfig} = require('@openedx/frontend-build'); 2 | 3 | const config = createConfig('webpack-dev') 4 | 5 | config.module.rules[0].exclude = /node_modules\/(?!(lodash-es|@(open)?edx))/ 6 | 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const {createConfig} = require('@openedx/frontend-build'); 2 | 3 | const config = createConfig('webpack-prod') 4 | 5 | config.module.rules[0].exclude = /node_modules\/(?!(lodash-es|@(open)?edx))/ 6 | 7 | module.exports = config; 8 | --------------------------------------------------------------------------------