diff --git a/src/discussions/common/ActionsDropdown.jsx b/src/discussions/common/ActionsDropdown.jsx
index 3b559b39..35912508 100644
--- a/src/discussions/common/ActionsDropdown.jsx
+++ b/src/discussions/common/ActionsDropdown.jsx
@@ -32,6 +32,11 @@ const ActionsDropdown = ({
const isPostingEnabled = useSelector(selectIsPostingEnabled);
const actions = useActions(contentType, id);
+ // Check if we're in in-context sidebar mode
+ const isInContextSidebar = useMemo(() => (
+ typeof window !== 'undefined' && window.location.search.includes('inContextSidebar')
+ ), []);
+
const handleActions = useCallback((action) => {
const actionFunction = actionHandlers[action];
if (actionFunction) {
@@ -59,6 +64,38 @@ const ActionsDropdown = ({
setTarget(null);
}, [close]);
+ const dropdownContent = (
+
+
-
- {actions.map(action => (
-
- {(action.action === ContentActions.DELETE) && }
- {
- close();
- handleActions(action.action);
- }}
- className="d-flex justify-content-start actions-dropdown-item"
- data-testId={action.id}
- >
-
-
- {intl.formatMessage(action.label)}
-
-
-
- ))}
-
+ {dropdownContent}
>
diff --git a/src/discussions/common/ActionsDropdown.test.jsx b/src/discussions/common/ActionsDropdown.test.jsx
index 6da4385f..a852a4ab 100644
--- a/src/discussions/common/ActionsDropdown.test.jsx
+++ b/src/discussions/common/ActionsDropdown.test.jsx
@@ -8,6 +8,7 @@ import { Factory } from 'rosie';
import { camelCaseObject, initializeMockApp, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { logError } from '@edx/frontend-platform/logging';
import { AppProvider } from '@edx/frontend-platform/react';
import { ContentActions } from '../../data/constants';
@@ -27,6 +28,11 @@ import ActionsDropdown from './ActionsDropdown';
import '../post-comments/data/__factories__';
import '../posts/data/__factories__';
+jest.mock('@edx/frontend-platform/logging', () => ({
+ ...jest.requireActual('@edx/frontend-platform/logging'),
+ logError: jest.fn(),
+}));
+
let store;
let axiosMock;
const commentsApiUrl = getCommentsApiUrl();
@@ -303,4 +309,148 @@ describe('ActionsDropdown', () => {
});
});
});
+
+ it('applies in-context-sidebar class when inContextSidebar is in URL', async () => {
+ const originalLocation = window.location;
+ delete window.location;
+ window.location = { ...originalLocation, search: '?inContextSidebar=true' };
+
+ const discussionObject = buildTestContent().discussion;
+ await mockThreadAndComment(discussionObject);
+
+ renderComponent({ ...camelCaseObject(discussionObject) });
+
+ const openButton = await findOpenActionsDropdownButton();
+ await act(async () => {
+ fireEvent.click(openButton);
+ });
+
+ const dropdown = screen.getByTestId('actions-dropdown-modal-popup').closest('.actions-dropdown');
+ expect(dropdown).toHaveClass('in-context-sidebar');
+
+ window.location = originalLocation;
+ });
+
+ it('does not apply in-context-sidebar class when inContextSidebar is not in URL', async () => {
+ const originalLocation = window.location;
+ delete window.location;
+ window.location = { ...originalLocation, search: '' };
+
+ const discussionObject = buildTestContent().discussion;
+ await mockThreadAndComment(discussionObject);
+
+ renderComponent({ ...camelCaseObject(discussionObject) });
+
+ const openButton = await findOpenActionsDropdownButton();
+ await act(async () => {
+ fireEvent.click(openButton);
+ });
+
+ const dropdown = screen.getByTestId('actions-dropdown-modal-popup').closest('.actions-dropdown');
+ expect(dropdown).not.toHaveClass('in-context-sidebar');
+
+ window.location = originalLocation;
+ });
+
+ it('handles SSR environment when window is undefined', () => {
+ const testSSRLogic = () => {
+ if (typeof window !== 'undefined') {
+ return window.location.search.includes('inContextSidebar');
+ }
+ return false;
+ };
+
+ const originalWindow = global.window;
+ const originalProcess = global.process;
+
+ try {
+ delete global.window;
+
+ const result = testSSRLogic();
+ expect(result).toBe(false);
+
+ global.window = originalWindow;
+ const resultWithWindow = testSSRLogic();
+ expect(resultWithWindow).toBe(false);
+ } finally {
+ global.window = originalWindow;
+ global.process = originalProcess;
+ }
+ });
+
+ it('calls logError for unknown action', async () => {
+ const discussionObject = buildTestContent().discussion;
+ await mockThreadAndComment(discussionObject);
+
+ logError.mockClear();
+
+ renderComponent({
+ ...discussionObject,
+ actionHandlers: {
+ [ContentActions.EDIT_CONTENT]: jest.fn(),
+ },
+ });
+
+ const openButton = await findOpenActionsDropdownButton();
+ await act(async () => {
+ fireEvent.click(openButton);
+ });
+
+ const copyLinkButton = await screen.findByText('Copy link');
+ await act(async () => {
+ fireEvent.click(copyLinkButton);
+ });
+
+ expect(logError).toHaveBeenCalledWith('Unknown or unimplemented action copy_link');
+ });
+
+ describe('posting restrictions', () => {
+ it('removes edit action when posting is disabled', async () => {
+ const discussionObject = buildTestContent({
+ editable_fields: ['raw_body'],
+ }).discussion;
+
+ await mockThreadAndComment(discussionObject);
+
+ axiosMock.onGet(`${getCourseConfigApiUrl()}${courseId}/`)
+ .reply(200, { isPostingEnabled: false });
+
+ await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
+
+ renderComponent({ ...discussionObject });
+
+ const openButton = await findOpenActionsDropdownButton();
+ await act(async () => {
+ fireEvent.click(openButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
+ });
+ });
+
+ it('keeps edit action when posting is enabled', async () => {
+ const discussionObject = buildTestContent({
+ editable_fields: ['raw_body'],
+ }).discussion;
+
+ await mockThreadAndComment(discussionObject);
+
+ axiosMock.onGet(`${getCourseConfigApiUrl()}${courseId}/`)
+ .reply(200, { isPostingEnabled: true });
+
+ await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
+
+ renderComponent({ ...discussionObject });
+
+ const openButton = await findOpenActionsDropdownButton();
+ await act(async () => {
+ fireEvent.click(openButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText('Edit')).toBeInTheDocument();
+ });
+ });
+ });
});
diff --git a/src/index.scss b/src/index.scss
index 2981b535..a51610e3 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -366,6 +366,11 @@ header {
z-index: 1;
}
+.actions-dropdown.in-context-sidebar {
+ position: fixed !important;
+ z-index: 10 !important;
+}
+
.discussion-topic-group:last-of-type .divider {
display: none;
}