Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions example-publish-button-slot.config.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import HidePublishButtonForStaff from './src/plugin-slots/CourseUnitPublishButtonSlot/example';

// Load environment variables from .env file
const config = {
...process.env,
pluginSlots: {
// Example: Hide the Publish button for staff users
'org.openedx.frontend.authoring.course_unit_publish_button.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Replace,
widget: {
id: 'hide_publish_for_staff',
type: DIRECT_PLUGIN,
RenderWidget: HidePublishButtonForStaff,
},
},
],
},
},
};

export default config;
27 changes: 26 additions & 1 deletion src/course-team/data/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';

import { USER_ROLES } from '../../constants';

Expand All @@ -19,6 +19,31 @@ export async function getCourseTeam(courseId) {
return camelCaseObject(data);
}

/**
* Get the current user's role for a course.
* @param {string} courseId
* @returns {Promise<{ role: ('instructor'|'staff'|null) }>}
*/
export async function getCourseUserRole(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseTeamApiUrl(courseId));

const camelCaseData = camelCaseObject(data);
const currentUser = getAuthenticatedUser();
const currentUserEmail = currentUser?.email;
const currentUsername = currentUser?.username;

// Find the current user in the users array
const currentUserInTeam = camelCaseData?.users?.find(
(user) => user.email === currentUserEmail || user.username === currentUsername,
);

// Return the role in the same format as before
return {
role: currentUserInTeam?.role || null,
};
}

/**
* Create course team user.
* @param {string} courseId
Expand Down
67 changes: 44 additions & 23 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ describe('<CourseUnit />', () => {
).toBeInTheDocument();
expect(
within(courseUnitSidebar).queryByRole('button', {
name: sidebarMessages.actionButtonPublishTitle.defaultMessage,
name: 'Publish',
}),
).toBeInTheDocument();
});
Expand Down Expand Up @@ -311,7 +311,7 @@ describe('<CourseUnit />', () => {
).toBeInTheDocument();
expect(
within(courseUnitSidebar).queryByRole('button', {
name: sidebarMessages.actionButtonPublishTitle.defaultMessage,
name: 'Publish',
}),
).toBeInTheDocument();
});
Expand Down Expand Up @@ -379,7 +379,7 @@ describe('<CourseUnit />', () => {
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByTestId('course-unit-publish-button')).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
});

Expand Down Expand Up @@ -427,7 +427,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByTestId('course-unit-publish-button')).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
Expand Down Expand Up @@ -508,7 +508,7 @@ describe('<CourseUnit />', () => {
});

await user.click(
await screen.findByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }),
await screen.findByRole('button', { name: 'Publish' }),
);

const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
Expand Down Expand Up @@ -548,7 +548,7 @@ describe('<CourseUnit />', () => {
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByTestId('course-unit-publish-button')).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
});

Expand All @@ -571,7 +571,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByTestId('course-unit-publish-button')).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
Expand Down Expand Up @@ -699,7 +699,7 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);

await waitFor(async () => {
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
await user.click(screen.getByTestId('course-unit-publish-button'));
});

axiosMock
Expand Down Expand Up @@ -743,7 +743,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByTestId('course-unit-publish-button')).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
Expand Down Expand Up @@ -884,7 +884,7 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);

await waitFor(async () => {
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
await user.click(screen.getByTestId('course-unit-publish-button'));
});

axiosMock
Expand Down Expand Up @@ -916,7 +916,7 @@ describe('<CourseUnit />', () => {
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByTestId('course-unit-publish-button')).not.toBeInTheDocument();

const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
Expand All @@ -925,20 +925,31 @@ describe('<CourseUnit />', () => {

await user.click(videoButton);

// Wait for the video modal to appear
await waitFor(() => {
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
});

// Close the modal by clicking the close button
const closeButtons = screen.getAllByRole('button', { name: 'Close' });
await user.click(closeButtons[closeButtons.length - 1]);

axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);

await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);

// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
});
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByTestId('course-unit-publish-button')).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
Expand All @@ -950,7 +961,6 @@ describe('<CourseUnit />', () => {
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
waffleSpy.mockRestore();
});

Expand All @@ -962,7 +972,7 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);

await waitFor(async () => {
await user.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
await user.click(screen.getByTestId('course-unit-publish-button'));
});

axiosMock
Expand Down Expand Up @@ -992,7 +1002,7 @@ describe('<CourseUnit />', () => {
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByTestId('course-unit-publish-button')).not.toBeInTheDocument();

const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
Expand All @@ -1003,10 +1013,19 @@ describe('<CourseUnit />', () => {
});

/** TODO -- fix this test.
The video editor modal opens but is stuck in loading state.
This needs proper API mocking for the video editor.
For now, we verify the modal opens and close it.
*/

// Wait for the editor modal to appear (it will show "Loading..." or "Editor Dialog")
await waitFor(() => {
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
expect(screen.getByRole('dialog', { name: /editor dialog/i })).toBeInTheDocument();
});
*/

// Close the modal by clicking the close button
const closeButtons = screen.getAllByRole('button', { name: 'Close' });
await user.click(closeButtons[closeButtons.length - 1]);

axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
Expand All @@ -1015,13 +1034,15 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);

// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
});
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByTestId('course-unit-publish-button')).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
Expand All @@ -1045,7 +1066,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByTestId('course-unit-publish-button')).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
Expand Down Expand Up @@ -1195,7 +1216,7 @@ describe('<CourseUnit />', () => {

await waitFor(async () => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
publishBtn = within(courseUnitSidebar).queryByTestId('course-unit-publish-button');
expect(publishBtn).toBeInTheDocument();

await user.click(publishBtn);
Expand Down Expand Up @@ -2338,7 +2359,7 @@ describe('<CourseUnit />', () => {
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
const publishButton = within(courseUnitSidebar).getByRole(
'button',
{ name: sidebarMessages.actionButtonPublishTitle.defaultMessage },
{ name: 'Publish' },
);
expect(publishButton).toBeInTheDocument();
expect(publishButton).toBeEnabled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { fetchCourseSectionVerticalData } from '../../../data/thunk';
import { courseSectionVerticalMock } from '../../../__mocks__';
import messages from '../../messages';
import ActionButtons from './ActionButtons';
import { getCourseTeamApiUrl } from '../../../../course-team/data/api';

let store;
let axiosMock;
Expand All @@ -38,6 +39,7 @@ describe('<ActionButtons />', () => {
authenticatedUser: {
userId: 3,
username: 'abc123',
email: 'abc123@example.com',
administrator: true,
roles: [],
},
Expand All @@ -54,6 +56,13 @@ describe('<ActionButtons />', () => {
enable_copy_paste_units: true,
},
});
axiosMock
.onGet(getCourseTeamApiUrl('course-v1:edX+DemoX+Demo_Course'))
.reply(200, {
users: [{ email: 'abc123@example.com', role: 'instructor', username: 'abc123' }],
allow_actions: true,
show_transfer_ownership_hint: false,
});
axiosMock
.onPost(getClipboardUrl())
.reply(200, clipboardUnit);
Expand Down
Loading
Loading