Skip to content

Commit

Permalink
refactor: [AXIMST-658] Course unit - Copy/paste refactoring (#204)
Browse files Browse the repository at this point in the history
* refactor: copy paste functional refactoring

* refactor: refactoring paste-button

* refactor: tests refactoring

* refactor: updated translations

* refactor: refactoring after review

* refactor: renamed status selector name
  • Loading branch information
PKulkoRaccoonGang authored and Kyrylo Hudym-Levkovych committed Mar 22, 2024
1 parent 50d2d0e commit 42038b3
Show file tree
Hide file tree
Showing 67 changed files with 259 additions and 547 deletions.
57 changes: 0 additions & 57 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
"@openedx/paragon": "^21.5.7",
"@reduxjs/toolkit": "1.9.7",
"@tanstack/react-query": "4.36.1",
"broadcast-channel": "^7.0.0",
"classnames": "2.2.6",
"core-js": "3.8.1",
"email-validator": "2.0.4",
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions src/__mocks__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as clipboardUnit } from './clipboardUnit';
export { default as clipboardXBlock } from './clipboardXBlock';
11 changes: 11 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,14 @@ export const COURSE_BLOCK_NAMES = /** @type {const} */ ({
});

export const UPLOAD_FILE_MAX_SIZE = 20 * 1000 * 1000; // 20mb

export const STUDIO_CLIPBOARD_CHANNEL = 'studio_clipboard_channel';

export const CLIPBOARD_STATUS = {
loading: 'loading',
ready: 'ready',
expired: 'expired',
error: 'error',
};

export const NOT_XBLOCK_TYPES = ['vertical', 'sequential', 'chapter', 'course'];
1 change: 0 additions & 1 deletion src/course-outline/CourseOutline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@
@import "./configure-modal/ConfigureModal";
@import "./drag-helper/SortableItem";
@import "./xblock-status/XBlockStatus";
@import "./paste-button/PasteButton";
46 changes: 19 additions & 27 deletions src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { cloneDeep } from 'lodash';
Expand All @@ -18,7 +18,6 @@ import {
getCourseBlockApiUrl,
getCourseItemApiUrl,
getXBlockBaseApiUrl,
getClipboardUrl,
} from './data/api';
import { RequestStatus } from '../data/constants';
import {
Expand All @@ -36,16 +35,18 @@ import {
courseSectionMock,
courseSubsectionMock,
} from './__mocks__';
import { clipboardUnit } from '../__mocks__';
import { executeThunk } from '../utils';
import { COURSE_BLOCK_NAMES, VIDEO_SHARING_OPTIONS } from './constants';
import CourseOutline from './CourseOutline';

import pasteButtonMessages from '../generic/clipboard/paste-component/messages';
import configureModalMessages from '../generic/configure-modal/messages';
import { getClipboardUrl } from '../generic/data/api';
import headerMessages from './header-navigations/messages';
import cardHeaderMessages from './card-header/messages';
import enableHighlightsModalMessages from './enable-highlights-modal/messages';
import statusBarMessages from './status-bar/messages';
import pasteButtonMessages from './paste-button/messages';
import subsectionMessages from './subsection-card/messages';
import pageAlertMessages from './page-alerts/messages';
import {
Expand All @@ -63,6 +64,13 @@ const courseId = '123';

window.HTMLElement.prototype.scrollIntoView = jest.fn();

const clipboardBroadcastChannelMock = {
postMessage: jest.fn(),
close: jest.fn(),
};

global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
Expand Down Expand Up @@ -2075,7 +2083,7 @@ describe('<CourseOutline />', () => {
});

it('check whether unit copy & paste option works correctly', async () => {
const { findAllByTestId, findAllByRole } = render(<RootWrapper />);
const { findAllByTestId, findAllByRole, queryByTestId } = render(<RootWrapper />);
// get first section -> first subsection -> first unit element
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [sectionElement] = await findAllByTestId('section-card');
Expand All @@ -2086,27 +2094,11 @@ describe('<CourseOutline />', () => {
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');

const expectedClipboardContent = {
content: {
blockType: 'vertical',
blockTypeDisplay: 'Unit',
created: '2024-01-29T07:58:36.844249Z',
displayName: unit.displayName,
id: 15,
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/15/olx',
purpose: 'clipboard',
status: 'ready',
userId: 3,
},
sourceUsageKey: unit.id,
sourceContexttitle: courseOutlineIndexMock.courseStructure.displayName,
sourceEditUrl: unit.studioUrl,
};
// mock api call
axiosMock
.onPost(getClipboardUrl(), {
usage_key: unit.id,
}).reply(200, expectedClipboardContent);
}).reply(200, clipboardUnit);
// check that initialUserClipboard state is empty
const { initialUserClipboard } = store.getState().courseOutline;
expect(initialUserClipboard).toBeUndefined();
Expand All @@ -2120,19 +2112,19 @@ describe('<CourseOutline />', () => {
await act(async () => fireEvent.click(copyButton));

// check that initialUserClipboard state is updated
expect(store.getState().courseOutline.initialUserClipboard).toEqual(expectedClipboardContent);
expect(store.getState().generic.clipboardData).toEqual(clipboardUnit);

[subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
// find clipboard content label
const clipboardLabel = await within(subsectionElement).findByText(
pasteButtonMessages.clipboardContentLabel.defaultMessage,
pasteButtonMessages.pasteButtonWhatsInClipboardText.defaultMessage,
);
await act(async () => fireEvent.mouseOver(clipboardLabel));

// find clipboard content popup link
expect(
subsectionElement.querySelector('#vertical-paste-button-overlay'),
).toHaveAttribute('href', unit.studioUrl);
// find clipboard content popover link
const popoverContent = queryByTestId('popover-content');
expect(popoverContent.tagName).toBe('A');
expect(popoverContent).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${unit.studioUrl}`);

// check paste button functionality
// mock api call
Expand Down
15 changes: 0 additions & 15 deletions src/course-outline/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${rein
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
export const getTagsCountApiUrl = (contentPattern) => new URL(`api/content_tagging/v1/object_tag_counts/${contentPattern}/?count_implicit`, getApiBaseUrl()).href;

/**
Expand Down Expand Up @@ -435,20 +434,6 @@ export async function setVideoSharingOption(courseId, videoSharingOption) {
return data;
}

/**
* Copy block to clipboard
* @param {string} usageKey
* @returns {Promise<Object>}
*/
export async function copyBlockToClipboard(usageKey) {
const { data } = await getAuthenticatedHttpClient()
.post(getClipboardUrl(), {
usage_key: usageKey,
});

return camelCaseObject(data);
}

/**
* Paste block to clipboard
* @param {string} parentLocator
Expand Down
1 change: 0 additions & 1 deletion src/course-outline/data/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ export const getCurrentSection = (state) => state.courseOutline.currentSection;
export const getCurrentSubsection = (state) => state.courseOutline.currentSubsection;
export const getCourseActions = (state) => state.courseOutline.actions;
export const getCustomRelativeDatesActiveFlag = (state) => state.courseOutline.isCustomRelativeDatesActive;
export const getInitialUserClipboard = (state) => state.courseOutline.initialUserClipboard;
export const getProctoredExamsFlag = (state) => state.courseOutline.enableProctoredExams;
export const getPasteFileNotices = (state) => state.courseOutline.pasteFileNotices;
11 changes: 0 additions & 11 deletions src/course-outline/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ const slice = createSlice({
childAddable: true,
duplicable: true,
},
initialUserClipboard: {
content: {},
sourceUsageKey: null,
sourceContexttitle: null,
sourceEditUrl: null,
},
enableProctoredExams: false,
pasteFileNotices: {},
},
Expand All @@ -52,7 +46,6 @@ const slice = createSlice({
state.outlineIndexData = payload;
state.sectionsList = payload.courseStructure?.childInfo?.children || [];
state.isCustomRelativeDatesActive = payload.isCustomRelativeDatesActive;
state.initialUserClipboard = payload.initialUserClipboard;
state.enableProctoredExams = payload.courseStructure?.enableProctoredExams;
},
updateOutlineIndexLoadingStatus: (state, { payload }) => {
Expand All @@ -79,9 +72,6 @@ const slice = createSlice({
...payload,
};
},
updateClipboardContent: (state, { payload }) => {
state.initialUserClipboard = payload;
},
updateCourseActions: (state, { payload }) => {
state.actions = {
...state.actions,
Expand Down Expand Up @@ -210,7 +200,6 @@ export const {
reorderSectionList,
reorderSubsectionList,
reorderUnitList,
updateClipboardContent,
setPasteFileNotices,
removePasteFileNotices,
} = slice.actions;
Expand Down
28 changes: 2 additions & 26 deletions src/course-outline/data/thunk.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RequestStatus } from '../../data/constants';
import { updateClipboardData } from '../../generic/data/slice';
import { NOTIFICATION_MESSAGES } from '../../constants';
import { COURSE_BLOCK_NAMES } from '../constants';
import {
Expand Down Expand Up @@ -28,7 +29,6 @@ import {
setSectionOrderList,
setVideoSharingOption,
setCourseItemOrderList,
copyBlockToClipboard,
pasteBlock,
dismissNotification,
} from './api';
Expand All @@ -50,7 +50,6 @@ import {
deleteUnit,
duplicateSection,
reorderSectionList,
updateClipboardContent,
setPasteFileNotices,
} from './slice';

Expand All @@ -70,6 +69,7 @@ export function fetchCourseOutlineIndexQuery(courseId) {
},
} = outlineIndex;
dispatch(fetchOutlineIndexSuccess(outlineIndex));
dispatch(updateClipboardData(outlineIndex.initialUserClipboard));
dispatch(updateStatusBar({
courseReleaseDate,
highlightsEnabledForMessaging,
Expand Down Expand Up @@ -607,30 +607,6 @@ export function setUnitOrderListQuery(
};
}

export function setClipboardContent(usageKey, broadcastClipboard) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.copying));

try {
await copyBlockToClipboard(usageKey).then(async (result) => {
const status = result?.content?.status;
if (status === 'ready') {
dispatch(updateClipboardContent(result));
broadcastClipboard(result);
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(hideProcessingNotification());
} else {
throw new Error(`Unexpected clipboard status "${status}" in successful API response.`);
}
});
} catch (error) {
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
}

export function pasteClipboardContent(parentLocator, sectionId) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
Expand Down
Loading

0 comments on commit 42038b3

Please sign in to comment.