diff --git a/example-publish-button-slot.config.jsx b/example-publish-button-slot.config.jsx
new file mode 100644
index 0000000000..ebfcef9a3f
--- /dev/null
+++ b/example-publish-button-slot.config.jsx
@@ -0,0 +1,25 @@
+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;
+
diff --git a/src/CourseAuthoringContext.tsx b/src/CourseAuthoringContext.tsx
index d497749500..2bd5af4cc6 100644
--- a/src/CourseAuthoringContext.tsx
+++ b/src/CourseAuthoringContext.tsx
@@ -26,6 +26,7 @@ type CourseAuthoringProviderProps = {
courseId: string;
};
+
export const CourseAuthoringProvider = ({
children,
courseId,
diff --git a/src/course-team/data/api.js b/src/course-team/data/api.js
index 321671600d..fd21cda689 100644
--- a/src/course-team/data/api.js
+++ b/src/course-team/data/api.js
@@ -1,10 +1,11 @@
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';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getCourseTeamApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_team/${courseId}`;
+export const getCourseRunApiUrl = (courseId) => `${getApiBaseUrl()}/api/v1/course_runs/${courseId}/`;
export const updateCourseTeamUserApiUrl = (courseId, email) => `${getApiBaseUrl()}/course_team/${courseId}/${email}`;
/**
@@ -19,6 +20,39 @@ export async function getCourseTeam(courseId) {
return camelCaseObject(data);
}
+/**
+ * Get the current user's role for a course using the course_runs API.
+ * This uses the existing /api/v1/course_runs/{course_id}/ endpoint.
+ * @param {string} courseId
+ * @returns {Promise<{ role: ('instructor'|'staff'|null) }>}
+ */
+export async function getCourseUserRole(courseId) {
+ try {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(getCourseRunApiUrl(courseId));
+
+ const camelCaseData = camelCaseObject(data);
+ const currentUser = getAuthenticatedUser();
+ const currentUsername = currentUser?.username;
+
+ // The course_runs API returns a 'team' array with user roles
+ const currentUserInTeam = camelCaseData?.team?.find(
+ (member) => member.user === currentUsername
+ );
+
+ // Return the role in the same format as before
+ return {
+ role: currentUserInTeam?.role || null,
+ };
+ } catch (error) {
+ // If there's an error (e.g., 404, 403), return null role
+ console.error('Error fetching user role from course_runs API:', error);
+ return {
+ role: null,
+ };
+ }
+}
+
/**
* Create course team user.
* @param {string} courseId
diff --git a/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.test.jsx b/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.test.jsx
index 652b0234d1..dd130491cf 100644
--- a/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.test.jsx
+++ b/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.test.jsx
@@ -16,6 +16,7 @@ import { fetchCourseSectionVerticalData } from '../../../data/thunk';
import { courseSectionVerticalMock } from '../../../__mocks__';
import messages from '../../messages';
import ActionButtons from './ActionButtons';
+import { getCourseRunApiUrl } from '../../../../course-team/data/api';
let store;
let axiosMock;
@@ -38,6 +39,7 @@ describe('', () => {
authenticatedUser: {
userId: 3,
username: 'abc123',
+ email: 'abc123@example.com',
administrator: true,
roles: [],
},
@@ -54,6 +56,11 @@ describe('', () => {
enable_copy_paste_units: true,
},
});
+ axiosMock
+ .onGet(getCourseRunApiUrl('course-v1:edX+DemoX+Demo_Course'))
+ .reply(200, {
+ team: [{ user: 'abc123', role: 'instructor' }],
+ });
axiosMock
.onPost(getClipboardUrl())
.reply(200, clipboardUnit);
diff --git a/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.tsx b/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.tsx
index 8bedc41c8b..36b9115b38 100644
--- a/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.tsx
+++ b/src/course-unit/sidebar/components/sidebar-footer/ActionButtons.tsx
@@ -1,17 +1,30 @@
+import { useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Button } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Divider } from '../../../../generic/divider';
-import { getCanEdit, getCourseUnitData } from '../../../data/selectors';
+import { getCanEdit, getCourseSectionVertical, getCourseUnitData } from '../../../data/selectors';
import { useClipboard } from '../../../../generic/clipboard';
+import { getCourseUserRole } from '../../../../course-team/data/api';
import messages from '../../messages';
+import CourseUnitPublishButtonSlot from '../../../../plugin-slots/CourseUnitPublishButtonSlot';
interface ActionButtonsProps {
openDiscardModal: () => void,
handlePublishing: () => void,
}
+type CourseRole = 'staff' | 'instructor' | null;
+
+const extractCourseKeyFromOutlineUrl = (outlineUrl?: string): string | null => {
+ if (!outlineUrl) {
+ return null;
+ }
+ const match = outlineUrl.match(/\/course\/([^?]+)/);
+ return match?.[1] ?? null;
+};
+
const ActionButtons = ({
openDiscardModal,
handlePublishing,
@@ -23,21 +36,58 @@ const ActionButtons = ({
hasChanges,
enableCopyPasteUnits,
} = useSelector(getCourseUnitData);
+ const courseSectionVertical = useSelector(getCourseSectionVertical);
const canEdit = useSelector(getCanEdit);
const { copyToClipboard } = useClipboard();
+ const [courseRole, setCourseRole] = useState(null);
+
+ const courseKey = useMemo(
+ () => extractCourseKeyFromOutlineUrl(courseSectionVertical?.outlineUrl),
+ [courseSectionVertical?.outlineUrl],
+ );
+
+ useEffect(() => {
+ // Only needed when the Publish button will render.
+ if (!(!published || hasChanges)) {
+ return;
+ }
+
+ if (!courseKey) {
+ return;
+ }
+
+ let cancelled = false;
+
+ (async () => {
+ try {
+ const { role } = await getCourseUserRole(courseKey) as any;
+ setCourseRole(role);
+ if (cancelled) return;
+ } catch (error: any) {
+ if (cancelled) return;
+ setCourseRole(null);
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [courseKey, hasChanges, published]);
+
+ const publishButtonText = courseRole
+ ? intl.formatMessage(messages.actionButtonPublishTitle, { role: courseRole })
+ : 'Publish';
+
return (
<>
- {(!published || hasChanges) && (
-
- )}
+
{(published && hasChanges) && (