Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
4d5c583
feat: moving allow public read switch from admin console (#2942)
jacobo-dominguez-wgu Mar 18, 2026
b2b5bc8
chore: update browserslist DB (#2952)
edx-requirements-bot Mar 23, 2026
0c59b78
chore(deps): update dependency oxlint-tsgolint to ^0.17.0 (#2953)
renovate[bot] Mar 24, 2026
652efb5
feat: remove lib v2 beta badge (#2951)
rpenido Mar 25, 2026
b3d25bd
fix: PR comments and cleanup
jesperhodge Mar 25, 2026
6f2f6b9
fix: typo
jesperhodge Mar 25, 2026
3a96b58
fix: types
jesperhodge Mar 25, 2026
9076a09
fix: remove deprecated settings waffle flags (#2959)
rpenido Mar 25, 2026
f193edb
fix: apply github code review suggestions
jesperhodge Mar 26, 2026
a8f8297
fix: PR comments
jesperhodge Mar 26, 2026
0971da4
fix: PR comments
jesperhodge Mar 26, 2026
825bab3
fix: tests
jesperhodge Mar 26, 2026
443221c
fix: tests
jesperhodge Mar 26, 2026
40f409e
fix: PR comment
jesperhodge Mar 26, 2026
4a4deef
fix: PR comments
jesperhodge Mar 26, 2026
9630215
feat: adds agreement-gated feature with support across files and vide…
xitij2000 Jan 28, 2026
38f87b8
fix: remove unused code
jesperhodge Mar 27, 2026
e8820a2
Merge remote-tracking branch 'upstream/master' into jhodge/create-tags
jesperhodge Mar 27, 2026
93605cd
Merge branch 'jhodge/create-tags' into temp--tag-table-base
jesperhodge Mar 27, 2026
726c5a7
feat: add button styling
jesperhodge Feb 26, 2026
2e77e19
feat: Add tags to a taxonomy (#2872)
jesperhodge Mar 27, 2026
7e0c15e
test: fix tests that are implemented
jesperhodge Feb 27, 2026
bfa7f72
feat: add reducer for table modes
jesperhodge Feb 27, 2026
b210695
feat: enable preview mode
jesperhodge Feb 27, 2026
14a036a
fix: mode transitions
jesperhodge Feb 27, 2026
ec0a690
feat: add row options menu
jesperhodge Feb 27, 2026
815d3d9
refactor: change table mode name to preview
jesperhodge Mar 2, 2026
3e8beba
refactor: extract subcomponents
jesperhodge Mar 2, 2026
c5b6916
refactor: extract table display component
jesperhodge Mar 2, 2026
f685672
refactor: make table components reusable
jesperhodge Mar 2, 2026
e54d369
refactor: simplify and extract components
jesperhodge Mar 2, 2026
942d374
refactor: extract reusable tree table components
jesperhodge Mar 2, 2026
6ffa46b
refactor: convert to typescript
jesperhodge Mar 2, 2026
be8dfd1
refactor: make tree table more readable
jesperhodge Mar 3, 2026
25a6d2c
fix: delete duplicate file
jesperhodge Mar 4, 2026
29b2af5
fix: expand rows style
jesperhodge Mar 4, 2026
cc70e72
chore(deps): update dependency @openedx/paragon to v23.19.2 (#2961)
renovate[bot] Mar 27, 2026
160c5fa
feat: attempt to make editable row
jesperhodge Mar 5, 2026
11910d1
chore(deps): update dependency @openedx/frontend-build to v14.6.3 (#2…
renovate[bot] Mar 27, 2026
3e3b3ce
feat: prettify expand all
jesperhodge Mar 6, 2026
67671d9
fix: transitions and styles
jesperhodge Mar 6, 2026
bb801cf
feat: #253 Initial commit for usage count display in taxonomy tags
tbain Mar 11, 2026
9cbc074
feat: #253 follow on commit to address GH Copilot suggestions
tbain Mar 11, 2026
1b7eee0
feat: #253 Add FE Unit tests, add invalidation logic on page load to …
tbain Mar 16, 2026
93e7235
chore(deps): update codemirror to v6.6.0 (#2944)
renovate[bot] Mar 27, 2026
37eca33
refactor: extract constants
jesperhodge Mar 17, 2026
9de8c51
feat: #253 Removing superfluous file, fixing paged query not using in…
tbain Mar 18, 2026
a8e4544
feat: #253 Fixing lint issues
tbain Mar 24, 2026
dbb02f3
feat: #253 Adding FE Unit tests, addition to add invalidation logic o…
tbain Mar 16, 2026
0d25d49
feat: #253 Fixing lint issues
tbain Mar 24, 2026
aedde44
feat: #253 Refactoring refresh logic to fix erroneous refresh, issues…
tbain Mar 25, 2026
d0d593e
chore(deps): update dependency @tanstack/react-query to v5.95.2 (#2954)
renovate[bot] Mar 27, 2026
799a8c8
chore(deps): bump minimatch from 3.1.2 to 3.1.5 (#2962)
dependabot[bot] Mar 27, 2026
f0d4611
feat: #253 Updating branch with latest from upstream
tbain Mar 27, 2026
96d6061
Merge branch 'master' of https://github.com/openedx/frontend-app-auth…
tbain Mar 27, 2026
9dabba6
feat: #253 re-adding missing updates after merge from upstream
tbain Mar 27, 2026
c34f928
docs: update course_unit_sidebar slot ID to v2 in plugin-slots README…
brian-smith-tcril Mar 30, 2026
807d785
chore(deps): update dependency oxlint to v1.57.0 (#2966)
renovate[bot] Mar 30, 2026
ba533a2
chore(deps): bump codecov/codecov-action from 5 to 6 (#2967)
dependabot[bot] Mar 30, 2026
87f8290
fix: validate authz perms with studio instead of LMS (#2947)
asadali145 Mar 30, 2026
b7955ce
chore: update browserslist DB (#2964)
edx-requirements-bot Mar 30, 2026
448fcad
feat: add deprecation warning to create legacy library form [FC-0123]…
rpenido Mar 30, 2026
4db8fca
feat: add course info settings sidebar [FC-0123] (#2955)
rpenido Mar 31, 2026
ec4cd64
fix: configure modal visibility tab background overflow (#2923)
asajjad2 Mar 31, 2026
e6ec144
Merge branch 'master' of https://github.com/openedx/frontend-app-auth…
tbain Apr 1, 2026
50812e2
feat: #253 Removing unneccessary file
tbain Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
path: coverage
merge-multiple: true
- name: Upload coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
448 changes: 205 additions & 243 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"@openedx/paragon": "^23.5.0",
"@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "2.11.2",
"@tanstack/react-query": "5.95.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-query": "5.90.21",
"@tinymce/tinymce-react": "^6.0.0",
"classnames": "2.5.1",
"codemirror": "^6.0.0",
Expand Down Expand Up @@ -120,7 +120,7 @@
"jest-canvas-mock": "^2.5.2",
"jest-expect-message": "^1.1.3",
"oxlint": "^1.42.0",
"oxlint-tsgolint": "^0.16.0",
"oxlint-tsgolint": "^0.17.0",
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}
Expand Down
6 changes: 4 additions & 2 deletions src/authz/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
PermissionValidationRequestItem,
PermissionValidationResponseItem,
} from '@src/authz/types';
import { getApiUrl } from './utils';
import { getConfig } from '@edx/frontend-platform';

export const getAuthzApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}/api/authz/${path || ''}`;

export const validateUserPermissions = async (
query: PermissionValidationQuery,
Expand All @@ -14,7 +16,7 @@ export const validateUserPermissions = async (
const request: PermissionValidationRequestItem[] = Object.values(query);

const { data }: { data: PermissionValidationResponseItem[] } = await getAuthenticatedHttpClient().post(
getApiUrl('/api/authz/v1/permissions/validate/me'),
getAuthzApiUrl('v1/permissions/validate/me'),
request,
);

Expand Down
4 changes: 0 additions & 4 deletions src/authz/data/utils.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@ export const BROKEN = 'broken';
export const LOCKED = 'locked';

export const MANUAL = 'manual';

export enum AgreementGated {
UPLOAD = 'upload',
UPLOAD_VIDEOS = 'upload.videos',
UPLOAD_FILES = 'upload.files',
}
11 changes: 4 additions & 7 deletions src/course-checklist/ChecklistSection/ChecklistItemBody.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,21 @@ import { ActionRow, Button, Icon } from '@openedx/paragon';
import { CheckCircle, RadioButtonUnchecked } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';

import { useWaffleFlags } from '../../data/apiHooks';
import { useWaffleFlags } from '@src/data/apiHooks';

import messages from './messages';

const getUpdateLinks = (courseId, waffleFlags) => {
const baseUrl = getConfig().STUDIO_BASE_URL;
const isLegacyGradingUrl = !waffleFlags.useNewGradingPage;
const isLegacyCertificateUrl = !waffleFlags.useNewCertificatesPage;
const isLegacyCourseDatesUrl = !waffleFlags.useNewScheduleDetailsPage;
const isLegacyOutlineUrl = !waffleFlags.useNewCourseOutlinePage;

return {
welcomeMessage: `/course/${courseId}/course_info`,
gradingPolicy: isLegacyGradingUrl
? `${baseUrl}/settings/grading/${courseId}` : `/course/${courseId}/settings/grading`,
gradingPolicy: `/course/${courseId}/settings/grading`,
certificate: isLegacyCertificateUrl
? `${baseUrl}/certificates/${courseId}` : `/course/${courseId}/certificates`,
courseDates: isLegacyCourseDatesUrl
? `${baseUrl}/settings/details/${courseId}#schedule` : `/course/${courseId}/settings/details/#schedule`,
courseDates: `/course/${courseId}/settings/details/#schedule`,
proctoringEmail: `${baseUrl}/pages-and-resources/proctoring/settings`,
outline: isLegacyOutlineUrl ? `${baseUrl}/course/${courseId}` : `/course/${courseId}`,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { camelCaseObject } from '@edx/frontend-platform';

import {
initializeMocks, render, screen, within,
} from '../../testUtils';
import { getApiWaffleFlagsUrl } from '../../data/api';
} from '@src/testUtils';
import { getApiWaffleFlagsUrl } from '@src/data/api';

import { generateCourseLaunchData } from '../factories/mockApiResponses';
import { checklistItems } from './utils/courseChecklistData';
import messages from './messages';
Expand Down Expand Up @@ -36,9 +37,7 @@ describe('ChecklistSection', () => {
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {
useNewGradingPage: true,
useNewCertificatesPage: true,
useNewScheduleDetailsPage: true,
useNewCourseOutlinePage: true,
});
});
Expand Down
5 changes: 5 additions & 0 deletions src/course-outline/CourseOutline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@ const CourseOutline = () => {
isOpen={isConfigureModalOpen}
onClose={handleConfigureModalClose}
onConfigureSubmit={handleConfigureItemSubmit}
/**
* Only sections need overflow visible (for the Release date datepicker, fixed in #2901);
* enabling it for subsection/unit modals causes the Visibility tab background to clip.
*/
isOverflowVisible={itemCategory === COURSE_BLOCK_NAMES.chapter.id}
currentItemData={currentItemData}
enableProctoredExams={enableProctoredExams}
enableTimedExams={enableTimedExams}
Expand Down
144 changes: 131 additions & 13 deletions src/course-outline/outline-sidebar/info-sidebar/CourseInfoSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle } from '@openedx/paragon';
import {
Tab,
Tabs,
useToggle,
} from '@openedx/paragon';
import { SchoolOutline, Tag } from '@openedx/paragon/icons';

import { useUserPermissions } from '@src/authz/data/apiHooks';
import { COURSE_PERMISSIONS } from '@src/authz/constants';
import { ContentTagsDrawerSheet, ContentTagsSnippet } from '@src/content-tags-drawer';
import { useCourseSettings, useWaffleFlags } from '@src/data/apiHooks';
import { ComponentCountSnippet } from '@src/generic/block-type-utils';
import { HelpSidebarLink, otherLinkURLParams, messages as helpSidebarMessages } from '@src/generic/help-sidebar';
import { SidebarContent, SidebarSection, SidebarTitle } from '@src/generic/sidebar';
import { useGetBlockTypes } from '@src/search-manager';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';

import { SidebarContent, SidebarSection, SidebarTitle } from '@src/generic/sidebar';

import { useCourseDetails } from '@src/course-outline/data/apiHooks';
import messages from '../messages';

export const CourseInfoSidebar = () => {
const DetailsTab = () => {
const intl = useIntl();
const { courseId } = useCourseAuthoringContext();
const { data: courseDetails } = useCourseDetails(courseId);

const { courseId } = useCourseAuthoringContext();
const { data: componentData } = useGetBlockTypes(
[`context_key = "${courseId}"`],
);

const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false);

return (
<div>
<SidebarTitle
title={courseDetails?.title || ''}
icon={SchoolOutline}
/>
<>
<SidebarContent>
<SidebarSection
title={intl.formatMessage(messages.sidebarSectionSummary)}
Expand All @@ -54,6 +56,122 @@ export const CourseInfoSidebar = () => {
onClose={closeManageTagsDrawer}
showSheet={isManageTagsDrawerOpen}
/>
</div>
</>
);
};

const SettingsTab = () => {
const intl = useIntl();
const { courseId } = useCourseAuthoringContext();
const { data: courseSettingsData } = useCourseSettings(courseId);

const {
grading,
courseTeam,
advancedSettings,
scheduleAndDetails,
groupConfigurations,
} = otherLinkURLParams;
const waffleFlags = useWaffleFlags(courseId);

const proctoredExamSettingsUrl = courseSettingsData?.mfeProctoredExamSettingsUrl;

/*
AuthZ for Course Authoring
If authz.enable_course_authoring flag is enabled, validate permissions using AuthZ API.
*/
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
canManageAdvancedSettings: {
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
scope: courseId,
},
}, isAuthzEnabled);

// If it's still loading, don't show the Advanced Settings link, otherwise, use the permission to decide
const authzCanManageAdvancedSettings = isLoadingUserPermissions
? false
: !!userPermissions?.canManageAdvancedSettings;

// When authz is enabled, use permission, otherwise it's always allowed (legacy behavior)
const canManageAdvancedSettings = isAuthzEnabled ? authzCanManageAdvancedSettings : true;

return (
<SidebarSection
title={intl.formatMessage(messages.settingsTabText)}
>
<HelpSidebarLink
as="span"
pathToPage={`/course/${courseId}/${scheduleAndDetails}`}
title={intl.formatMessage(
helpSidebarMessages.sidebarLinkToScheduleAndDetails,
)}
isNewPage
/>
<HelpSidebarLink
as="span"
pathToPage={`/course/${courseId}/${grading}`}
title={intl.formatMessage(helpSidebarMessages.sidebarLinkToGrading)}
isNewPage
/>
<HelpSidebarLink
as="span"
pathToPage={`/course/${courseId}/${courseTeam}`}
title={intl.formatMessage(helpSidebarMessages.sidebarLinkToCourseTeam)}
isNewPage
/>
<HelpSidebarLink
as="span"
pathToPage={`/course/${courseId}/${groupConfigurations}`}
title={intl.formatMessage(helpSidebarMessages.sidebarLinkToGroupConfigurations)}
isNewPage
/>
{canManageAdvancedSettings && (
<HelpSidebarLink
as="span"
pathToPage={`/course/${courseId}/${advancedSettings}`}
title={intl.formatMessage(helpSidebarMessages.sidebarLinkToAdvancedSettings)}
isNewPage
/>
)}
{proctoredExamSettingsUrl && (
<HelpSidebarLink
as="span"
pathToPage={proctoredExamSettingsUrl}
title={intl.formatMessage(
helpSidebarMessages.sidebarLinkToProctoredExamSettings,
)}
isNewPage
/>
)}
</SidebarSection>
);
};

export const CourseInfoSidebar = () => {
const intl = useIntl();
const { courseId } = useCourseAuthoringContext();
const { data: courseDetails } = useCourseDetails(courseId);

return (
<>
<SidebarTitle
title={courseDetails?.title || ''}
icon={SchoolOutline}
/>
<Tabs
variant="tabs"
className="my-2 mx-n3.5"
id="course-info-tabs"
mountOnEnter
>
<Tab eventKey="info" title={intl.formatMessage(messages.infoTabText)}>
<DetailsTab />
</Tab>
<Tab eventKey="settings" title={intl.formatMessage(messages.settingsTabText)}>
<SettingsTab />
</Tab>
</Tabs>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { initializeMocks, render, screen } from '@src/testUtils';
import { SelectionState } from '@src/data/types';
import { getCourseSettingsApiUrl } from '@src/data/api';
import type { SelectionState } from '@src/data/types';
import { OutlineSidebarProvider } from '@src/course-outline/outline-sidebar/OutlineSidebarContext';
import { getXBlockApiUrl } from '@src/course-outline/data/api';
import userEvent from '@testing-library/user-event';
Expand All @@ -22,10 +23,12 @@ jest.mock('@src/course-outline/data/apiHooks', () => ({
}),
}));

const courseId = '5';

const openPublishModal = jest.fn();
jest.mock('@src/CourseAuthoringContext', () => ({
useCourseAuthoringContext: () => ({
courseId: 5,
courseId,
setCurrentSelection: jest.fn(),
openPublishModal,
getUnitUrl: jest.fn(),
Expand All @@ -50,6 +53,32 @@ describe('InfoSidebar component', () => {
expect(await screen.findByText('Course name')).toBeInTheDocument();
});

it('shows the settings link for the course', async () => {
const user = userEvent.setup();
renderComponent();
await user.click((await screen.findByRole('tab', { name: 'Settings' })));
const links = await screen.findAllByRole('link');
expect(links).toHaveLength(5);
expect(links[0]).toHaveTextContent('Schedule & details');
expect(links[1]).toHaveTextContent('Grading');
expect(links[2]).toHaveTextContent('Course team');
expect(links[3]).toHaveTextContent('Group configurations');
expect(links[4]).toHaveTextContent('Advanced settings');
});

it('shows the proctored exam settings link for the course if it exists', async () => {
const user = userEvent.setup();
const courseSettingsData = {
mfeProctoredExamSettingsUrl: 'https://example.com/proctored-exam-settings',
};
axiosMock
.onGet(getCourseSettingsApiUrl(courseId))
.reply(200, courseSettingsData);
renderComponent();
await user.click(await screen.findByRole('tab', { name: 'Settings' }));
expect(await screen.findByRole('link', { name: 'Proctored exam settings' })).toBeInTheDocument();
});

it('renders InfoSidebar with section info', async () => {
const user = userEvent.setup();
selectedContainerState = {
Expand Down
5 changes: 5 additions & 0 deletions src/course-outline/page-alerts/PageAlerts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { usePasteFileNotices } from '@src/course-outline/data/apiHooks';
import { AlertAgreementGatedFeature } from '@src/generic/agreement-gated-feature';
import { AgreementGated } from '../../constants';
import CourseOutlinePageAlertsSlot from '../../plugin-slots/CourseOutlinePageAlertsSlot';
import advancedSettingsMessages from '../../advanced-settings/messages';
import { OutOfSyncAlert } from '../../course-libraries/OutOfSyncAlert';
Expand Down Expand Up @@ -441,6 +443,9 @@ const PageAlerts = ({
{conflictingFilesPasteAlert()}
{newFilesPasteAlert()}
{renderOutOfSyncAlert()}
<AlertAgreementGatedFeature
gatingTypes={[AgreementGated.UPLOAD, AgreementGated.UPLOAD_VIDEOS, AgreementGated.UPLOAD_FILES]}
/>
<CourseOutlinePageAlertsSlot />
</>
);
Expand Down
Loading
Loading