Compare commits
40 Commits
master
...
release/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14e122a672 | ||
|
|
f459f53343 | ||
|
|
a5a7d03d12 | ||
|
|
41fc478efe | ||
|
|
06497bf85c | ||
|
|
7e0b7f94e8 | ||
|
|
4bc34c268b | ||
|
|
2973614e3b | ||
|
|
bdc99fddc3 | ||
|
|
92c59cbf0c | ||
|
|
b6bd94c114 | ||
|
|
c9896a8fe5 | ||
|
|
4ba8cde587 | ||
|
|
86d0a7e7db | ||
|
|
1968d146cd | ||
|
|
3e737b5b0d | ||
|
|
fcdf1fdecb | ||
|
|
efb1a28b4d | ||
|
|
1ff5e5bdae | ||
|
|
19ef80553a | ||
|
|
2beb91c63b | ||
|
|
d325a92204 | ||
|
|
7dfd93d4f1 | ||
|
|
e34df7f270 | ||
|
|
317bc757cf | ||
|
|
212a54f76e | ||
|
|
944d1316ad | ||
|
|
dd731a0d19 | ||
|
|
976dfcaab7 | ||
|
|
403dfa1e6b | ||
|
|
1919eb4845 | ||
|
|
3d6e221f99 | ||
|
|
fab786a6c6 | ||
|
|
a162929fd7 | ||
|
|
6c4634ebbe | ||
|
|
79f865b328 | ||
|
|
d5e36cf2b8 | ||
|
|
8ffafc094f | ||
|
|
b375806fd2 | ||
|
|
ab0e0d71c1 |
@@ -10,4 +10,5 @@ coverage:
|
|||||||
threshold: 0%
|
threshold: 0%
|
||||||
ignore:
|
ignore:
|
||||||
- "src/grading-settings/grading-scale/react-ranger.js"
|
- "src/grading-settings/grading-scale/react-ranger.js"
|
||||||
|
- "src/generic/DraggableList/verticalSortableList.ts"
|
||||||
- "src/index.js"
|
- "src/index.js"
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import {
|
||||||
|
camelCaseObject,
|
||||||
|
getConfig,
|
||||||
|
} from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { camelCase } from 'lodash';
|
||||||
import { convertObjectToSnakeCase } from '../../utils';
|
import { convertObjectToSnakeCase } from '../../utils';
|
||||||
|
|
||||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||||
@@ -14,7 +19,19 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/
|
|||||||
export async function getCourseAdvancedSettings(courseId) {
|
export async function getCourseAdvancedSettings(courseId) {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
|
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
|
||||||
return camelCaseObject(data);
|
const keepValues = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
keepValues[camelCase(key)] = { value: data[key].value };
|
||||||
|
});
|
||||||
|
const formattedData = {};
|
||||||
|
const formattedCamelCaseData = camelCaseObject(data);
|
||||||
|
Object.keys(formattedCamelCaseData).forEach((key) => {
|
||||||
|
formattedData[key] = {
|
||||||
|
...formattedCamelCaseData[key],
|
||||||
|
value: keepValues[key]?.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return formattedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +43,19 @@ export async function getCourseAdvancedSettings(courseId) {
|
|||||||
export async function updateCourseAdvancedSettings(courseId, settings) {
|
export async function updateCourseAdvancedSettings(courseId, settings) {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
|
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
|
||||||
return camelCaseObject(data);
|
const keepValues = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
keepValues[camelCase(key)] = { value: data[key].value };
|
||||||
|
});
|
||||||
|
const formattedData = {};
|
||||||
|
const formattedCamelCaseData = camelCaseObject(data);
|
||||||
|
Object.keys(formattedCamelCaseData).forEach((key) => {
|
||||||
|
formattedData[key] = {
|
||||||
|
...formattedCamelCaseData[key],
|
||||||
|
value: keepValues[key]?.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return formattedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,5 +65,17 @@ export async function updateCourseAdvancedSettings(courseId, settings) {
|
|||||||
*/
|
*/
|
||||||
export async function getProctoringExamErrors(courseId) {
|
export async function getProctoringExamErrors(courseId) {
|
||||||
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
|
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
|
||||||
return camelCaseObject(data);
|
const keepValues = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
keepValues[camelCase(key)] = { value: data[key].value };
|
||||||
|
});
|
||||||
|
const formattedData = {};
|
||||||
|
const formattedCamelCaseData = camelCaseObject(data);
|
||||||
|
Object.keys(formattedCamelCaseData).forEach((key) => {
|
||||||
|
formattedData[key] = {
|
||||||
|
...formattedCamelCaseData[key],
|
||||||
|
value: keepValues[key]?.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return formattedData;
|
||||||
}
|
}
|
||||||
|
|||||||
236
src/advanced-settings/data/api.test.js
Normal file
236
src/advanced-settings/data/api.test.js
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import {
|
||||||
|
getCourseAdvancedSettings,
|
||||||
|
updateCourseAdvancedSettings,
|
||||||
|
getProctoringExamErrors,
|
||||||
|
} from './api';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getAuthenticatedHttpClient: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('courseSettings API', () => {
|
||||||
|
const mockHttpClient = {
|
||||||
|
get: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCourseAdvancedSettings', () => {
|
||||||
|
it('should fetch and unformat course advanced settings', async () => {
|
||||||
|
const fakeData = {
|
||||||
|
key_snake_case: {
|
||||||
|
display_name: 'To come camelCase',
|
||||||
|
testCamelCase: 'This key must not be formatted',
|
||||||
|
PascalCase: 'To come camelCase',
|
||||||
|
'kebab-case': 'To come camelCase',
|
||||||
|
UPPER_CASE: 'To come camelCase',
|
||||||
|
lowercase: 'This key must not be formatted',
|
||||||
|
UPPERCASE: 'To come lowercase',
|
||||||
|
'Title Case': 'To come camelCase',
|
||||||
|
'dot.case': 'To come camelCase',
|
||||||
|
SCREAMING_SNAKE_CASE: 'To come camelCase',
|
||||||
|
MixedCase: 'To come camelCase',
|
||||||
|
'Train-Case': 'To come camelCase',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'To come camelCase',
|
||||||
|
},
|
||||||
|
// value is an object with various cases
|
||||||
|
// this contain must not be formatted to camelCase
|
||||||
|
value: {
|
||||||
|
snake_case: 'snake_case',
|
||||||
|
camelCase: 'camelCase',
|
||||||
|
PascalCase: 'PascalCase',
|
||||||
|
'kebab-case': 'kebab-case',
|
||||||
|
UPPER_CASE: 'UPPER_CASE',
|
||||||
|
lowercase: 'lowercase',
|
||||||
|
UPPERCASE: 'UPPERCASE',
|
||||||
|
'Title Case': 'Title Case',
|
||||||
|
'dot.case': 'dot.case',
|
||||||
|
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
|
||||||
|
MixedCase: 'MixedCase',
|
||||||
|
'Train-Case': 'Train-Case',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'nestedContent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
keySnakeCase: {
|
||||||
|
displayName: 'To come camelCase',
|
||||||
|
testCamelCase: 'This key must not be formatted',
|
||||||
|
pascalCase: 'To come camelCase',
|
||||||
|
kebabCase: 'To come camelCase',
|
||||||
|
upperCase: 'To come camelCase',
|
||||||
|
lowercase: 'This key must not be formatted',
|
||||||
|
uppercase: 'To come lowercase',
|
||||||
|
titleCase: 'To come camelCase',
|
||||||
|
dotCase: 'To come camelCase',
|
||||||
|
screamingSnakeCase: 'To come camelCase',
|
||||||
|
mixedCase: 'To come camelCase',
|
||||||
|
trainCase: 'To come camelCase',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'To come camelCase',
|
||||||
|
},
|
||||||
|
value: fakeData.key_snake_case.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockHttpClient.get.mockResolvedValue({ data: fakeData });
|
||||||
|
|
||||||
|
const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024');
|
||||||
|
expect(mockHttpClient.get).toHaveBeenCalledWith(
|
||||||
|
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateCourseAdvancedSettings', () => {
|
||||||
|
it('should update and unformat course advanced settings', async () => {
|
||||||
|
const fakeData = {
|
||||||
|
key_snake_case: {
|
||||||
|
display_name: 'To come camelCase',
|
||||||
|
testCamelCase: 'This key must not be formatted', // because already be camelCase
|
||||||
|
PascalCase: 'To come camelCase',
|
||||||
|
'kebab-case': 'To come camelCase',
|
||||||
|
UPPER_CASE: 'To come camelCase',
|
||||||
|
lowercase: 'This key must not be formatted', // because camelCase in lowercase not formatted
|
||||||
|
UPPERCASE: 'To come lowercase', // because camelCase in UPPERCASE format to lowercase
|
||||||
|
'Title Case': 'To come camelCase',
|
||||||
|
'dot.case': 'To come camelCase',
|
||||||
|
SCREAMING_SNAKE_CASE: 'To come camelCase',
|
||||||
|
MixedCase: 'To come camelCase',
|
||||||
|
'Train-Case': 'To come camelCase',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'To come camelCase',
|
||||||
|
},
|
||||||
|
// value is an object with various cases
|
||||||
|
// this contain must not be formatted to camelCase
|
||||||
|
value: {
|
||||||
|
snake_case: 'snake_case',
|
||||||
|
camelCase: 'camelCase',
|
||||||
|
PascalCase: 'PascalCase',
|
||||||
|
'kebab-case': 'kebab-case',
|
||||||
|
UPPER_CASE: 'UPPER_CASE',
|
||||||
|
lowercase: 'lowercase',
|
||||||
|
UPPERCASE: 'UPPERCASE',
|
||||||
|
'Title Case': 'Title Case',
|
||||||
|
'dot.case': 'dot.case',
|
||||||
|
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
|
||||||
|
MixedCase: 'MixedCase',
|
||||||
|
'Train-Case': 'Train-Case',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'nestedContent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
keySnakeCase: {
|
||||||
|
displayName: 'To come camelCase',
|
||||||
|
testCamelCase: 'This key must not be formatted',
|
||||||
|
pascalCase: 'To come camelCase',
|
||||||
|
kebabCase: 'To come camelCase',
|
||||||
|
upperCase: 'To come camelCase',
|
||||||
|
lowercase: 'This key must not be formatted',
|
||||||
|
uppercase: 'To come lowercase',
|
||||||
|
titleCase: 'To come camelCase',
|
||||||
|
dotCase: 'To come camelCase',
|
||||||
|
screamingSnakeCase: 'To come camelCase',
|
||||||
|
mixedCase: 'To come camelCase',
|
||||||
|
trainCase: 'To come camelCase',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'To come camelCase',
|
||||||
|
},
|
||||||
|
value: fakeData.key_snake_case.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockHttpClient.patch.mockResolvedValue({ data: fakeData });
|
||||||
|
|
||||||
|
const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {});
|
||||||
|
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
||||||
|
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getProctoringExamErrors', () => {
|
||||||
|
it('should fetch proctoring errors and return unformat object', async () => {
|
||||||
|
const fakeData = {
|
||||||
|
key_snake_case: {
|
||||||
|
display_name: 'To come camelCase',
|
||||||
|
testCamelCase: 'This key must not be formatted',
|
||||||
|
PascalCase: 'To come camelCase',
|
||||||
|
'kebab-case': 'To come camelCase',
|
||||||
|
UPPER_CASE: 'To come camelCase',
|
||||||
|
lowercase: 'This key must not be formatted',
|
||||||
|
UPPERCASE: 'To come lowercase',
|
||||||
|
'Title Case': 'To come camelCase',
|
||||||
|
'dot.case': 'To come camelCase',
|
||||||
|
SCREAMING_SNAKE_CASE: 'To come camelCase',
|
||||||
|
MixedCase: 'To come camelCase',
|
||||||
|
'Train-Case': 'To come camelCase',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'To come camelCase',
|
||||||
|
},
|
||||||
|
// value is an object with various cases
|
||||||
|
// this contain must not be formatted to camelCase
|
||||||
|
value: {
|
||||||
|
snake_case: 'snake_case',
|
||||||
|
camelCase: 'camelCase',
|
||||||
|
PascalCase: 'PascalCase',
|
||||||
|
'kebab-case': 'kebab-case',
|
||||||
|
UPPER_CASE: 'UPPER_CASE',
|
||||||
|
lowercase: 'lowercase',
|
||||||
|
UPPERCASE: 'UPPERCASE',
|
||||||
|
'Title Case': 'Title Case',
|
||||||
|
'dot.case': 'dot.case',
|
||||||
|
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
|
||||||
|
MixedCase: 'MixedCase',
|
||||||
|
'Train-Case': 'Train-Case',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'nestedContent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
keySnakeCase: {
|
||||||
|
displayName: 'To come camelCase',
|
||||||
|
testCamelCase: 'This key must not be formatted',
|
||||||
|
pascalCase: 'To come camelCase',
|
||||||
|
kebabCase: 'To come camelCase',
|
||||||
|
upperCase: 'To come camelCase',
|
||||||
|
lowercase: 'This key must not be formatted',
|
||||||
|
uppercase: 'To come lowercase',
|
||||||
|
titleCase: 'To come camelCase',
|
||||||
|
dotCase: 'To come camelCase',
|
||||||
|
screamingSnakeCase: 'To come camelCase',
|
||||||
|
mixedCase: 'To come camelCase',
|
||||||
|
trainCase: 'To come camelCase',
|
||||||
|
nestedOption: {
|
||||||
|
anotherOption: 'To come camelCase',
|
||||||
|
},
|
||||||
|
value: fakeData.key_snake_case.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockHttpClient.get.mockResolvedValue({ data: fakeData });
|
||||||
|
|
||||||
|
const result = await getProctoringExamErrors('course-v1:Test+T101+2024');
|
||||||
|
expect(mockHttpClient.get).toHaveBeenCalledWith(
|
||||||
|
`${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ const path = '/content/:contentId?/*';
|
|||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
const mockSetBlockingSheet = jest.fn();
|
const mockSetBlockingSheet = jest.fn();
|
||||||
const mockNavigate = jest.fn();
|
const mockNavigate = jest.fn();
|
||||||
|
const mockSidebarAction = jest.fn();
|
||||||
mockContentTaxonomyTagsData.applyMock();
|
mockContentTaxonomyTagsData.applyMock();
|
||||||
mockTaxonomyListData.applyMock();
|
mockTaxonomyListData.applyMock();
|
||||||
mockTaxonomyTagsData.applyMock();
|
mockTaxonomyTagsData.applyMock();
|
||||||
@@ -40,6 +41,11 @@ jest.mock('react-router-dom', () => ({
|
|||||||
useNavigate: () => mockNavigate,
|
useNavigate: () => mockNavigate,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../library-authoring/common/context/SidebarContext', () => ({
|
||||||
|
...jest.requireActual('../library-authoring/common/context/SidebarContext'),
|
||||||
|
useSidebarContext: () => ({ sidebarAction: mockSidebarAction() }),
|
||||||
|
}));
|
||||||
|
|
||||||
const renderDrawer = (contentId, drawerParams = {}) => (
|
const renderDrawer = (contentId, drawerParams = {}) => (
|
||||||
render(
|
render(
|
||||||
<ContentTagsDrawerSheetContext.Provider value={drawerParams}>
|
<ContentTagsDrawerSheetContext.Provider value={drawerParams}>
|
||||||
@@ -184,6 +190,26 @@ describe('<ContentTagsDrawer />', () => {
|
|||||||
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should change to edit mode sidebar action is set to JumpToManageTags', async () => {
|
||||||
|
mockSidebarAction.mockReturnValueOnce('jump-to-manage-tags');
|
||||||
|
renderDrawer(stagedTagsId, { variant: 'component' });
|
||||||
|
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Show delete tag buttons
|
||||||
|
expect(screen.getAllByRole('button', {
|
||||||
|
name: /delete/i,
|
||||||
|
}).length).toBe(2);
|
||||||
|
|
||||||
|
// Show add a tag select
|
||||||
|
expect(screen.getByText(/add a tag/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Show cancel button
|
||||||
|
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Show save button
|
||||||
|
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should change to read mode when click on `Cancel` on drawer variant', async () => {
|
it('should change to read mode when click on `Cancel` on drawer variant', async () => {
|
||||||
renderDrawer(stagedTagsId);
|
renderDrawer(stagedTagsId);
|
||||||
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
|
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ContentTagsCollapsible from './ContentTagsCollapsible';
|
|||||||
import Loading from '../generic/Loading';
|
import Loading from '../generic/Loading';
|
||||||
import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper';
|
import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper';
|
||||||
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
|
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
|
||||||
|
import { SidebarActions, useSidebarContext } from '../library-authoring/common/context/SidebarContext';
|
||||||
|
|
||||||
interface TaxonomyListProps {
|
interface TaxonomyListProps {
|
||||||
contentId: string;
|
contentId: string;
|
||||||
@@ -244,6 +245,7 @@ const ContentTagsDrawer = ({
|
|||||||
if (contentId === undefined) {
|
if (contentId === undefined) {
|
||||||
throw new Error('Error: contentId cannot be null.');
|
throw new Error('Error: contentId cannot be null.');
|
||||||
}
|
}
|
||||||
|
const { sidebarAction } = useSidebarContext();
|
||||||
|
|
||||||
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
|
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
|
||||||
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
|
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
|
||||||
@@ -260,6 +262,7 @@ const ContentTagsDrawer = ({
|
|||||||
closeToast,
|
closeToast,
|
||||||
setCollapsibleToInitalState,
|
setCollapsibleToInitalState,
|
||||||
otherTaxonomies,
|
otherTaxonomies,
|
||||||
|
toEditMode,
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
let onCloseDrawer: () => void;
|
let onCloseDrawer: () => void;
|
||||||
@@ -302,8 +305,13 @@ const ContentTagsDrawer = ({
|
|||||||
|
|
||||||
// First call of the initial collapsible states
|
// First call of the initial collapsible states
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setCollapsibleToInitalState();
|
// Open tag edit mode when sidebarAction is JumpToManageTags
|
||||||
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
|
if (sidebarAction === SidebarActions.JumpToManageTags) {
|
||||||
|
toEditMode();
|
||||||
|
} else {
|
||||||
|
setCollapsibleToInitalState();
|
||||||
|
}
|
||||||
|
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, sidebarAction, toEditMode]);
|
||||||
|
|
||||||
const renderFooter = () => {
|
const renderFooter = () => {
|
||||||
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
|
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
import {
|
import {
|
||||||
getTaxonomyTagsData,
|
getTaxonomyTagsData,
|
||||||
getContentTaxonomyTagsData,
|
getContentTaxonomyTagsData,
|
||||||
@@ -14,7 +15,7 @@ import {
|
|||||||
updateContentTaxonomyTags,
|
updateContentTaxonomyTags,
|
||||||
getContentTaxonomyTagsCount,
|
getContentTaxonomyTagsCount,
|
||||||
} from './api';
|
} from './api';
|
||||||
import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
|
import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
|
||||||
import { getLibraryId } from '../../generic/key-utils';
|
import { getLibraryId } from '../../generic/key-utils';
|
||||||
|
|
||||||
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
|
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
|
||||||
@@ -129,6 +130,7 @@ export const useContentData = (contentId, enabled) => (
|
|||||||
export const useContentTaxonomyTagsUpdater = (contentId) => {
|
export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const unitIframe = window.frames['xblock-iframe'];
|
const unitIframe = window.frames['xblock-iframe'];
|
||||||
|
const { unitId } = useParams();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
/**
|
/**
|
||||||
@@ -158,6 +160,10 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
|||||||
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
|
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
|
||||||
// Invalidate content search to update tags count
|
// Invalidate content search to update tags count
|
||||||
queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
|
queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
|
||||||
|
// If the tags for a compoent were edited from Unit page, invalidate children query to fetch count again.
|
||||||
|
if (unitId) {
|
||||||
|
queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: /* istanbul ignore next */ () => {
|
onSuccess: /* istanbul ignore next */ () => {
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ describe('useContentTaxonomyTagsUpdater', () => {
|
|||||||
|
|
||||||
const contentId = 'testerContent';
|
const contentId = 'testerContent';
|
||||||
const taxonomyId = 123;
|
const taxonomyId = 123;
|
||||||
const mutation = useContentTaxonomyTagsUpdater(contentId);
|
const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current;
|
||||||
const tagsData = [{
|
const tagsData = [{
|
||||||
taxonomy: taxonomyId,
|
taxonomy: taxonomyId,
|
||||||
tags: ['tag1', 'tag2'],
|
tags: ['tag1', 'tag2'],
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe('<CourseLibraries />', () => {
|
|||||||
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
|
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
userEvent.click(allTab);
|
userEvent.click(allTab);
|
||||||
const alert = await screen.findByRole('alert');
|
const alert = (await screen.findAllByRole('alert'))[0];
|
||||||
expect(await within(alert).findByText(
|
expect(await within(alert).findByText(
|
||||||
'5 library components are out of sync. Review updates to accept or ignore changes',
|
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
@@ -105,7 +105,7 @@ describe('<CourseLibraries />', () => {
|
|||||||
userEvent.click(allTab);
|
userEvent.click(allTab);
|
||||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
const alert = await screen.findByRole('alert');
|
const alert = (await screen.findAllByRole('alert'))[0];
|
||||||
expect(await within(alert).findByText(
|
expect(await within(alert).findByText(
|
||||||
'5 library components are out of sync. Review updates to accept or ignore changes',
|
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
@@ -118,6 +118,46 @@ describe('<CourseLibraries />', () => {
|
|||||||
userEvent.click(reviewActionBtn);
|
userEvent.click(reviewActionBtn);
|
||||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
|
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('show alert if max lastPublishedDate is greated than the local storage value', async () => {
|
||||||
|
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
|
||||||
|
localStorage.setItem(
|
||||||
|
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
|
||||||
|
String(lastPublishedDate.getTime() - 1000),
|
||||||
|
);
|
||||||
|
|
||||||
|
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
|
||||||
|
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
|
||||||
|
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
|
||||||
|
// review tab should be open by default as outOfSyncCount is greater than 0
|
||||||
|
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
|
userEvent.click(allTab);
|
||||||
|
const alert = (await screen.findAllByRole('alert'))[0];
|
||||||
|
expect(await within(alert).findByText(
|
||||||
|
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||||
|
)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesnt show alert if max lastPublishedDate is less than the local storage value', async () => {
|
||||||
|
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
|
||||||
|
localStorage.setItem(
|
||||||
|
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
|
||||||
|
String(lastPublishedDate.getTime() + 1000),
|
||||||
|
);
|
||||||
|
|
||||||
|
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
|
||||||
|
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
|
||||||
|
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
|
||||||
|
// review tab should be open by default as outOfSyncCount is greater than 0
|
||||||
|
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
|
||||||
|
userEvent.click(allTab);
|
||||||
|
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
|
screen.logTestingPlaygroundURL();
|
||||||
|
|
||||||
|
expect(screen.queryAllByRole('alert').length).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<CourseLibraries ReviewTab />', () => {
|
describe('<CourseLibraries ReviewTab />', () => {
|
||||||
@@ -160,7 +200,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
|||||||
|
|
||||||
it('update changes works', async () => {
|
it('update changes works', async () => {
|
||||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||||
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
|
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
|
||||||
@@ -176,7 +216,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
|||||||
|
|
||||||
it('update changes works in preview modal', async () => {
|
it('update changes works in preview modal', async () => {
|
||||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||||
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
||||||
@@ -195,7 +235,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
|||||||
|
|
||||||
it('ignore change works', async () => {
|
it('ignore change works', async () => {
|
||||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||||
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
||||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||||
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
|
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
|
||||||
@@ -218,7 +258,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
|||||||
|
|
||||||
it('ignore change works in preview', async () => {
|
it('ignore change works in preview', async () => {
|
||||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||||
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
||||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||||
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import {
|
import {
|
||||||
Cached, CheckCircle, Launch, Loop,
|
Cached, CheckCircle, Launch, Loop, Info,
|
||||||
} from '@openedx/paragon/icons';
|
} from '@openedx/paragon/icons';
|
||||||
|
|
||||||
import sumBy from 'lodash/sumBy';
|
import sumBy from 'lodash/sumBy';
|
||||||
@@ -33,6 +33,7 @@ import { useStudioHome } from '../studio-home/hooks';
|
|||||||
import NewsstandIcon from '../generic/NewsstandIcon';
|
import NewsstandIcon from '../generic/NewsstandIcon';
|
||||||
import ReviewTabContent from './ReviewTabContent';
|
import ReviewTabContent from './ReviewTabContent';
|
||||||
import { OutOfSyncAlert } from './OutOfSyncAlert';
|
import { OutOfSyncAlert } from './OutOfSyncAlert';
|
||||||
|
import AlertMessage from '../generic/alert-message';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
courseId: string;
|
courseId: string;
|
||||||
@@ -164,7 +165,7 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
|||||||
if (tabKey !== CourseLibraryTabs.review) {
|
if (tabKey !== CourseLibraryTabs.review) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!outOfSyncCount || outOfSyncCount === 0) {
|
if (!outOfSyncCount) {
|
||||||
return (
|
return (
|
||||||
<Stack direction="horizontal" gap={2}>
|
<Stack direction="horizontal" gap={2}>
|
||||||
<Icon src={CheckCircle} size="xs" />
|
<Icon src={CheckCircle} size="xs" />
|
||||||
@@ -199,6 +200,12 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
|||||||
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
|
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
|
||||||
setShowAlert={setShowReviewAlert}
|
setShowAlert={setShowReviewAlert}
|
||||||
/>
|
/>
|
||||||
|
{ /* TODO: Remove this alert after implement container in this page */}
|
||||||
|
<AlertMessage
|
||||||
|
title={intl.formatMessage(messages.unitsUpdatesWarning)}
|
||||||
|
icon={Info}
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
<SubHeader
|
<SubHeader
|
||||||
title={intl.formatMessage(messages.headingTitle)}
|
title={intl.formatMessage(messages.headingTitle)}
|
||||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||||
|
|||||||
@@ -18,12 +18,11 @@ interface OutOfSyncAlertProps {
|
|||||||
* in course can be updated. Following are the conditions for displaying the alert.
|
* in course can be updated. Following are the conditions for displaying the alert.
|
||||||
*
|
*
|
||||||
* * The alert is displayed if components are out of sync.
|
* * The alert is displayed if components are out of sync.
|
||||||
* * If the user clicks on dismiss button, the state is stored in localstorage of user
|
* * If the user clicks on dismiss button, the state of dismiss is stored in localstorage of user
|
||||||
* in this format: outOfSyncCountAlert-${courseId} = <number of out of sync components>.
|
* in this format: outOfSyncCountAlert-${courseId} = <datetime value in milliseconds>.
|
||||||
* * If the number of sync components don't change for the course and the user opens outline
|
* * If there are not new published components for the course and the user opens outline
|
||||||
* in the same browser, they don't see the alert again.
|
* in the same browser, they don't see the alert again.
|
||||||
* * If the number changes, i.e., if a new component is out of sync or the user updates or ignores
|
* * If there is a new published component upstream, the alert is displayed again.
|
||||||
* a component, the alert is displayed again.
|
|
||||||
*/
|
*/
|
||||||
export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
||||||
showAlert,
|
showAlert,
|
||||||
@@ -35,6 +34,8 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
|
const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
|
||||||
const outOfSyncCount = data?.reduce((count, lib) => count + (lib.readyToSyncCount || 0), 0);
|
const outOfSyncCount = data?.reduce((count, lib) => count + (lib.readyToSyncCount || 0), 0);
|
||||||
|
const lastPublishedDate = data?.map(lib => new Date(lib.lastPublishedAt || 0).getTime())
|
||||||
|
.reduce((acc, lastPublished) => Math.max(lastPublished, acc), 0);
|
||||||
const alertKey = `outOfSyncCountAlert-${courseId}`;
|
const alertKey = `outOfSyncCountAlert-${courseId}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -46,13 +47,14 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
|||||||
setShowAlert(false);
|
setShowAlert(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dismissedAlert = localStorage.getItem(alertKey);
|
const dismissedAlertDate = parseInt(localStorage.getItem(alertKey) ?? '0', 10);
|
||||||
setShowAlert(parseInt(dismissedAlert || '', 10) !== outOfSyncCount);
|
|
||||||
}, [outOfSyncCount, isLoading, data]);
|
setShowAlert((lastPublishedDate ?? 0) > dismissedAlertDate);
|
||||||
|
}, [outOfSyncCount, lastPublishedDate, isLoading, data]);
|
||||||
|
|
||||||
const dismissAlert = () => {
|
const dismissAlert = () => {
|
||||||
setShowAlert(false);
|
setShowAlert(false);
|
||||||
localStorage.setItem(alertKey, String(outOfSyncCount));
|
localStorage.setItem(alertKey, Date.now().toString());
|
||||||
onDismiss?.();
|
onDismiss?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
useCallback, useContext, useEffect, useMemo, useState,
|
useCallback, useContext, useMemo, useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
@@ -14,11 +14,9 @@ import {
|
|||||||
useToggle,
|
useToggle,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
|
|
||||||
import {
|
import { tail, keyBy } from 'lodash';
|
||||||
tail, keyBy, orderBy, merge, omitBy,
|
|
||||||
} from 'lodash';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Loop, Warning } from '@openedx/paragon/icons';
|
import { Loop } from '@openedx/paragon/icons';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import previewChangesMessages from '../course-unit/preview-changes/messages';
|
import previewChangesMessages from '../course-unit/preview-changes/messages';
|
||||||
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
|
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
|
||||||
@@ -37,7 +35,6 @@ import { useLoadOnScroll } from '../hooks';
|
|||||||
import DeleteModal from '../generic/delete-modal/DeleteModal';
|
import DeleteModal from '../generic/delete-modal/DeleteModal';
|
||||||
import { PublishableEntityLink } from './data/api';
|
import { PublishableEntityLink } from './data/api';
|
||||||
import AlertError from '../generic/alert-error';
|
import AlertError from '../generic/alert-error';
|
||||||
import AlertMessage from '../generic/alert-message';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
courseId: string;
|
courseId: string;
|
||||||
@@ -102,10 +99,8 @@ const BlockCard: React.FC<BlockCardProps> = ({ info, actions }) => {
|
|||||||
|
|
||||||
const ComponentReviewList = ({
|
const ComponentReviewList = ({
|
||||||
outOfSyncComponents,
|
outOfSyncComponents,
|
||||||
onSearchUpdate,
|
|
||||||
}: {
|
}: {
|
||||||
outOfSyncComponents: PublishableEntityLink[];
|
outOfSyncComponents: PublishableEntityLink[];
|
||||||
onSearchUpdate: () => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { showToast } = useContext(ToastContext);
|
const { showToast } = useContext(ToastContext);
|
||||||
@@ -113,24 +108,15 @@ const ComponentReviewList = ({
|
|||||||
// ignore changes confirmation modal toggle.
|
// ignore changes confirmation modal toggle.
|
||||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
||||||
const {
|
const {
|
||||||
hits: downstreamInfo,
|
hits,
|
||||||
isLoading: isIndexDataLoading,
|
isLoading: isIndexDataLoading,
|
||||||
searchKeywords,
|
|
||||||
searchSortOrder,
|
|
||||||
hasError,
|
hasError,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = useSearchContext() as {
|
} = useSearchContext();
|
||||||
hits: ContentHit[];
|
|
||||||
isLoading: boolean;
|
const downstreamInfo = hits as ContentHit[];
|
||||||
searchKeywords: string;
|
|
||||||
searchSortOrder: SearchSortOption;
|
|
||||||
hasError: boolean;
|
|
||||||
hasNextPage: boolean | undefined,
|
|
||||||
isFetchingNextPage: boolean;
|
|
||||||
fetchNextPage: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
useLoadOnScroll(
|
useLoadOnScroll(
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
@@ -143,24 +129,14 @@ const ComponentReviewList = ({
|
|||||||
() => keyBy(outOfSyncComponents, 'downstreamUsageKey'),
|
() => keyBy(outOfSyncComponents, 'downstreamUsageKey'),
|
||||||
[outOfSyncComponents],
|
[outOfSyncComponents],
|
||||||
);
|
);
|
||||||
const downstreamInfoByKey = useMemo(
|
|
||||||
() => keyBy(downstreamInfo, 'usageKey'),
|
|
||||||
[downstreamInfo],
|
|
||||||
);
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchKeywords) {
|
|
||||||
onSearchUpdate();
|
|
||||||
}
|
|
||||||
}, [searchKeywords]);
|
|
||||||
|
|
||||||
// Toggle preview changes modal
|
// Toggle preview changes modal
|
||||||
const [isModalOpen, openModal, closeModal] = useToggle(false);
|
const [isModalOpen, openModal, closeModal] = useToggle(false);
|
||||||
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
||||||
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
||||||
|
|
||||||
const setSeletecdBlockData = (info: ContentHit) => {
|
const setSelectedBlockData = useCallback((info: ContentHit) => {
|
||||||
setBlockData({
|
setBlockData({
|
||||||
displayName: info.displayName,
|
displayName: info.displayName,
|
||||||
downstreamBlockId: info.usageKey,
|
downstreamBlockId: info.usageKey,
|
||||||
@@ -168,17 +144,18 @@ const ComponentReviewList = ({
|
|||||||
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
|
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
|
||||||
isVertical: info.blockType === 'vertical',
|
isVertical: info.blockType === 'vertical',
|
||||||
});
|
});
|
||||||
};
|
}, [outOfSyncComponentsByKey]);
|
||||||
|
|
||||||
// Show preview changes on review
|
// Show preview changes on review
|
||||||
const onReview = useCallback((info: ContentHit) => {
|
const onReview = useCallback((info: ContentHit) => {
|
||||||
setSeletecdBlockData(info);
|
setSelectedBlockData(info);
|
||||||
openModal();
|
openModal();
|
||||||
}, [setSeletecdBlockData, openModal]);
|
}, [setSelectedBlockData, openModal]);
|
||||||
|
|
||||||
const onIgnoreClick = useCallback((info: ContentHit) => {
|
const onIgnoreClick = useCallback((info: ContentHit) => {
|
||||||
setSeletecdBlockData(info);
|
setSelectedBlockData(info);
|
||||||
openConfirmModal();
|
openConfirmModal();
|
||||||
}, [setSeletecdBlockData, openConfirmModal]);
|
}, [setSelectedBlockData, openConfirmModal]);
|
||||||
|
|
||||||
const reloadLinks = useCallback((usageKey: string) => {
|
const reloadLinks = useCallback((usageKey: string) => {
|
||||||
const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey;
|
const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey;
|
||||||
@@ -236,19 +213,6 @@ const ComponentReviewList = ({
|
|||||||
}
|
}
|
||||||
}, [blockData]);
|
}, [blockData]);
|
||||||
|
|
||||||
const orderInfo = useMemo(() => {
|
|
||||||
if (searchSortOrder !== SearchSortOption.RECENTLY_MODIFIED) {
|
|
||||||
return downstreamInfo;
|
|
||||||
}
|
|
||||||
if (isIndexDataLoading) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
let merged = merge(downstreamInfoByKey, outOfSyncComponentsByKey);
|
|
||||||
merged = omitBy(merged, (o) => !o.displayName);
|
|
||||||
const ordered = orderBy(Object.values(merged), 'updated', 'desc');
|
|
||||||
return ordered;
|
|
||||||
}, [downstreamInfoByKey, outOfSyncComponentsByKey]);
|
|
||||||
|
|
||||||
if (isIndexDataLoading) {
|
if (isIndexDataLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
@@ -259,7 +223,7 @@ const ComponentReviewList = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{orderInfo?.map((info) => (
|
{downstreamInfo?.map((info) => (
|
||||||
<BlockCard
|
<BlockCard
|
||||||
key={info.usageKey}
|
key={info.usageKey}
|
||||||
info={info}
|
info={info}
|
||||||
@@ -293,20 +257,14 @@ const ComponentReviewList = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<PreviewLibraryXBlockChanges
|
{blockData && (
|
||||||
blockData={blockData}
|
<PreviewLibraryXBlockChanges
|
||||||
isModalOpen={isModalOpen}
|
blockData={blockData}
|
||||||
closeModal={closeModal}
|
isModalOpen={isModalOpen}
|
||||||
postChange={postChange}
|
closeModal={closeModal}
|
||||||
alertNode={(
|
postChange={postChange}
|
||||||
<AlertMessage
|
/>
|
||||||
show
|
)}
|
||||||
variant="warning"
|
|
||||||
icon={Warning}
|
|
||||||
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
isOpen={isConfirmModalOpen}
|
isOpen={isConfirmModalOpen}
|
||||||
close={closeConfirmModal}
|
close={closeConfirmModal}
|
||||||
@@ -323,37 +281,17 @@ const ComponentReviewList = ({
|
|||||||
const ReviewTabContent = ({ courseId }: Props) => {
|
const ReviewTabContent = ({ courseId }: Props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
data: linkPages,
|
data: outOfSyncComponents,
|
||||||
isLoading: isSyncComponentsLoading,
|
isLoading: isSyncComponentsLoading,
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useEntityLinks({ courseId, readyToSync: true });
|
} = useEntityLinks({ courseId, readyToSync: true });
|
||||||
|
|
||||||
const outOfSyncComponents = useMemo(
|
|
||||||
() => linkPages?.pages?.reduce((links, page) => [...links, ...page.results], []) ?? [],
|
|
||||||
[linkPages],
|
|
||||||
);
|
|
||||||
const downstreamKeys = useMemo(
|
const downstreamKeys = useMemo(
|
||||||
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
|
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
|
||||||
[outOfSyncComponents],
|
[outOfSyncComponents],
|
||||||
);
|
);
|
||||||
|
|
||||||
useLoadOnScroll(
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSearchUpdate = () => {
|
|
||||||
if (hasNextPage && !isFetchingNextPage) {
|
|
||||||
fetchNextPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableSortOptions = [
|
const disableSortOptions = [
|
||||||
SearchSortOption.RELEVANCE,
|
SearchSortOption.RELEVANCE,
|
||||||
SearchSortOption.OLDEST,
|
SearchSortOption.OLDEST,
|
||||||
@@ -384,7 +322,6 @@ const ReviewTabContent = ({ courseId }: Props) => {
|
|||||||
</ActionRow>
|
</ActionRow>
|
||||||
<ComponentReviewList
|
<ComponentReviewList
|
||||||
outOfSyncComponents={outOfSyncComponents}
|
outOfSyncComponents={outOfSyncComponents}
|
||||||
onSearchUpdate={onSearchUpdate}
|
|
||||||
/>
|
/>
|
||||||
</SearchContextProvider>
|
</SearchContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,17 +3,20 @@
|
|||||||
"upstreamContextTitle": "CS problems 3",
|
"upstreamContextTitle": "CS problems 3",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||||
"readyToSyncCount": 5,
|
"readyToSyncCount": 5,
|
||||||
"totalCount": 14
|
"totalCount": 14,
|
||||||
|
"lastPublishedAt": "2025-05-01T20:20:44.989042Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"upstreamContextTitle": "CS problems 2",
|
"upstreamContextTitle": "CS problems 2",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||||
"readyToSyncCount": 0,
|
"readyToSyncCount": 0,
|
||||||
"totalCount": 21
|
"totalCount": 21,
|
||||||
|
"lastPublishedAt": "2025-05-01T21:20:44.989042Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"upstreamContextTitle": "CS problems",
|
"upstreamContextTitle": "CS problems",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB",
|
"upstreamContextKey": "lib:OpenedX:CSPROB",
|
||||||
"totalCount": 3
|
"totalCount": 3,
|
||||||
|
"lastPublishedAt": "2025-05-01T22:20:44.989042Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,79 +1,72 @@
|
|||||||
{
|
[
|
||||||
"count": 7,
|
{
|
||||||
"next": null,
|
"id": 875,
|
||||||
"previous": null,
|
"upstreamContextTitle": "CS problems 3",
|
||||||
"num_pages": 1,
|
"upstreamVersion": 10,
|
||||||
"current_page": 1,
|
"readyToSync": true,
|
||||||
"results": [
|
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||||
{
|
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||||
"id": 875,
|
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
||||||
"upstreamContextTitle": "CS problems 3",
|
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||||
"upstreamVersion": 10,
|
"versionSynced": 2,
|
||||||
"readyToSync": true,
|
"versionDeclined": null,
|
||||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
"created": "2025-02-08T14:07:05.588484Z",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
"updated": "2025-02-08T14:07:05.588484Z"
|
||||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
},
|
||||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
{
|
||||||
"versionSynced": 2,
|
"id": 876,
|
||||||
"versionDeclined": null,
|
"upstreamContextTitle": "CS problems 3",
|
||||||
"created": "2025-02-08T14:07:05.588484Z",
|
"upstreamVersion": 10,
|
||||||
"updated": "2025-02-08T14:07:05.588484Z"
|
"readyToSync": true,
|
||||||
},
|
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||||
{
|
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||||
"id": 876,
|
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
||||||
"upstreamContextTitle": "CS problems 3",
|
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||||
"upstreamVersion": 10,
|
"versionSynced": 2,
|
||||||
"readyToSync": true,
|
"versionDeclined": null,
|
||||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
"created": "2025-02-08T14:07:05.588484Z",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
"updated": "2025-02-08T14:07:05.588484Z"
|
||||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
},
|
||||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
{
|
||||||
"versionSynced": 2,
|
"id": 884,
|
||||||
"versionDeclined": null,
|
"upstreamContextTitle": "CS problems 3",
|
||||||
"created": "2025-02-08T14:07:05.588484Z",
|
"upstreamVersion": 26,
|
||||||
"updated": "2025-02-08T14:07:05.588484Z"
|
"readyToSync": true,
|
||||||
},
|
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||||
{
|
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||||
"id": 884,
|
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
||||||
"upstreamContextTitle": "CS problems 3",
|
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||||
"upstreamVersion": 26,
|
"versionSynced": 16,
|
||||||
"readyToSync": true,
|
"versionDeclined": null,
|
||||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
"created": "2025-02-08T14:07:05.588484Z",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
"updated": "2025-02-08T14:07:05.588484Z"
|
||||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
},
|
||||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
{
|
||||||
"versionSynced": 16,
|
"id": 889,
|
||||||
"versionDeclined": null,
|
"upstreamContextTitle": "CS problems 3",
|
||||||
"created": "2025-02-08T14:07:05.588484Z",
|
"upstreamVersion": 10,
|
||||||
"updated": "2025-02-08T14:07:05.588484Z"
|
"readyToSync": true,
|
||||||
},
|
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||||
{
|
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||||
"id": 889,
|
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
||||||
"upstreamContextTitle": "CS problems 3",
|
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||||
"upstreamVersion": 10,
|
"versionSynced": 2,
|
||||||
"readyToSync": true,
|
"versionDeclined": null,
|
||||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
"created": "2025-02-08T14:07:05.588484Z",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
"updated": "2025-02-08T14:07:05.588484Z"
|
||||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
},
|
||||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
{
|
||||||
"versionSynced": 2,
|
"id": 890,
|
||||||
"versionDeclined": null,
|
"upstreamContextTitle": "CS problems 3",
|
||||||
"created": "2025-02-08T14:07:05.588484Z",
|
"upstreamVersion": 10,
|
||||||
"updated": "2025-02-08T14:07:05.588484Z"
|
"readyToSync": true,
|
||||||
},
|
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||||
{
|
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||||
"id": 890,
|
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
||||||
"upstreamContextTitle": "CS problems 3",
|
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||||
"upstreamVersion": 10,
|
"versionSynced": 2,
|
||||||
"readyToSync": true,
|
"versionDeclined": null,
|
||||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
"created": "2025-02-08T14:07:05.588484Z",
|
||||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
"updated": "2025-02-08T14:07:05.588484Z"
|
||||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
}
|
||||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
]
|
||||||
"versionSynced": 2,
|
|
||||||
"versionDeclined": null,
|
|
||||||
"created": "2025-02-08T14:07:05.588484Z",
|
|
||||||
"updated": "2025-02-08T14:07:05.588484Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,27 +28,17 @@ export async function mockGetEntityLinks(
|
|||||||
case mockGetEntityLinks.courseKeyLoading:
|
case mockGetEntityLinks.courseKeyLoading:
|
||||||
return new Promise(() => {});
|
return new Promise(() => {});
|
||||||
case mockGetEntityLinks.courseKeyEmpty:
|
case mockGetEntityLinks.courseKeyEmpty:
|
||||||
return Promise.resolve({
|
return Promise.resolve([]);
|
||||||
next: null,
|
|
||||||
previous: null,
|
|
||||||
nextPageNum: null,
|
|
||||||
previousPageNum: null,
|
|
||||||
count: 0,
|
|
||||||
numPages: 0,
|
|
||||||
currentPage: 0,
|
|
||||||
results: [],
|
|
||||||
});
|
|
||||||
default: {
|
default: {
|
||||||
const { response } = mockGetEntityLinks;
|
let { response } = mockGetEntityLinks;
|
||||||
if (readyToSync !== undefined) {
|
if (readyToSync !== undefined) {
|
||||||
response.results = response.results.filter((o) => o.readyToSync === readyToSync);
|
response = response.filter((o) => o.readyToSync === readyToSync);
|
||||||
response.count = response.results.length;
|
|
||||||
}
|
}
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mockGetEntityLinks.courseKey = mockLinksResult.results[0].downstreamContextKey;
|
mockGetEntityLinks.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||||
mockGetEntityLinks.invalidCourseKey = 'course_key_error';
|
mockGetEntityLinks.invalidCourseKey = 'course_key_error';
|
||||||
mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading';
|
mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading';
|
||||||
mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty';
|
mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty';
|
||||||
@@ -85,7 +75,7 @@ export async function mockGetEntityLinksSummaryByDownstreamContext(
|
|||||||
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response);
|
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult.results[0].downstreamContextKey;
|
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||||
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error';
|
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error';
|
||||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading';
|
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading';
|
||||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
|
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
|
||||||
|
|||||||
@@ -38,32 +38,13 @@ export interface PublishableEntityLinkSummary {
|
|||||||
upstreamContextTitle: string;
|
upstreamContextTitle: string;
|
||||||
readyToSyncCount: number;
|
readyToSyncCount: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
lastPublishedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEntityLinks = async (
|
export const getEntityLinks = async (
|
||||||
downstreamContextKey?: string,
|
downstreamContextKey?: string,
|
||||||
readyToSync?: boolean,
|
readyToSync?: boolean,
|
||||||
upstreamUsageKey?: string,
|
upstreamUsageKey?: string,
|
||||||
pageParam?: number,
|
|
||||||
pageSize?: number,
|
|
||||||
): Promise<PaginatedData<PublishableEntityLink[]>> => {
|
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
|
||||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
|
||||||
params: {
|
|
||||||
course_id: downstreamContextKey,
|
|
||||||
ready_to_sync: readyToSync,
|
|
||||||
upstream_usage_key: upstreamUsageKey,
|
|
||||||
page_size: pageSize,
|
|
||||||
page: pageParam,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return camelCaseObject(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUnpaginatedEntityLinks = async (
|
|
||||||
downstreamContextKey?: string,
|
|
||||||
readyToSync?: boolean,
|
|
||||||
upstreamUsageKey?: string,
|
|
||||||
): Promise<PublishableEntityLink[]> => {
|
): Promise<PublishableEntityLink[]> => {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
.get(getEntityLinksByDownstreamContextUrl(), {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import { renderHook, waitFor } from '@testing-library/react';
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
import { getEntityLinksByDownstreamContextUrl } from './api';
|
import { getEntityLinksByDownstreamContextUrl } from './api';
|
||||||
import { useEntityLinks, useUnpaginatedEntityLinks } from './apiHooks';
|
import { useEntityLinks } from './apiHooks';
|
||||||
|
|
||||||
let axiosMock: MockAdapter;
|
let axiosMock: MockAdapter;
|
||||||
|
|
||||||
@@ -39,26 +39,11 @@ describe('course libraries api hooks', () => {
|
|||||||
axiosMock.reset();
|
axiosMock.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return paginated links for course', async () => {
|
|
||||||
const courseId = 'course-v1:some+key';
|
|
||||||
const url = getEntityLinksByDownstreamContextUrl();
|
|
||||||
const expectedResult = {
|
|
||||||
next: null, results: [], previous: null, total: 0,
|
|
||||||
};
|
|
||||||
axiosMock.onGet(url).reply(200, expectedResult);
|
|
||||||
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.isLoading).toBeFalsy();
|
|
||||||
});
|
|
||||||
expect(result.current.data?.pages).toEqual([expectedResult]);
|
|
||||||
expect(axiosMock.history.get[0].url).toEqual(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return links for course', async () => {
|
it('should return links for course', async () => {
|
||||||
const courseId = 'course-v1:some+key';
|
const courseId = 'course-v1:some+key';
|
||||||
const url = getEntityLinksByDownstreamContextUrl();
|
const url = getEntityLinksByDownstreamContextUrl();
|
||||||
axiosMock.onGet(url).reply(200, []);
|
axiosMock.onGet(url).reply(200, []);
|
||||||
const { result } = renderHook(() => useUnpaginatedEntityLinks({ courseId }), { wrapper });
|
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.isLoading).toBeFalsy();
|
expect(result.current.isLoading).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
useInfiniteQuery,
|
|
||||||
useQuery,
|
useQuery,
|
||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext, getUnpaginatedEntityLinks } from './api';
|
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api';
|
||||||
|
|
||||||
export const courseLibrariesQueryKeys = {
|
export const courseLibrariesQueryKeys = {
|
||||||
all: ['courseLibraries'],
|
all: ['courseLibraries'],
|
||||||
@@ -29,39 +28,10 @@ export const courseLibrariesQueryKeys = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to fetch publishable entity links by course key.
|
* Hook to fetch list of publishable entity links by course key.
|
||||||
* (That is, get a list of the library components used in the given course.)
|
* (That is, get a list of the library components used in the given course.)
|
||||||
*/
|
*/
|
||||||
export const useEntityLinks = ({
|
export const useEntityLinks = ({
|
||||||
courseId, readyToSync, upstreamUsageKey, pageSize,
|
|
||||||
}: {
|
|
||||||
courseId?: string,
|
|
||||||
readyToSync?: boolean,
|
|
||||||
upstreamUsageKey?: string,
|
|
||||||
pageSize?: number
|
|
||||||
}) => (
|
|
||||||
useInfiniteQuery({
|
|
||||||
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
|
|
||||||
courseId,
|
|
||||||
readyToSync,
|
|
||||||
upstreamUsageKey,
|
|
||||||
}),
|
|
||||||
queryFn: ({ pageParam }) => getEntityLinks(
|
|
||||||
courseId,
|
|
||||||
readyToSync,
|
|
||||||
upstreamUsageKey,
|
|
||||||
pageParam,
|
|
||||||
pageSize,
|
|
||||||
),
|
|
||||||
getNextPageParam: (lastPage) => lastPage.nextPageNum,
|
|
||||||
enabled: courseId !== undefined || upstreamUsageKey !== undefined || readyToSync !== undefined,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to fetch unpaginated list of publishable entity links by course key.
|
|
||||||
*/
|
|
||||||
export const useUnpaginatedEntityLinks = ({
|
|
||||||
courseId, readyToSync, upstreamUsageKey,
|
courseId, readyToSync, upstreamUsageKey,
|
||||||
}: {
|
}: {
|
||||||
courseId?: string,
|
courseId?: string,
|
||||||
@@ -74,7 +44,7 @@ export const useUnpaginatedEntityLinks = ({
|
|||||||
readyToSync,
|
readyToSync,
|
||||||
upstreamUsageKey,
|
upstreamUsageKey,
|
||||||
}),
|
}),
|
||||||
queryFn: () => getUnpaginatedEntityLinks(
|
queryFn: () => getEntityLinks(
|
||||||
courseId,
|
courseId,
|
||||||
readyToSync,
|
readyToSync,
|
||||||
upstreamUsageKey,
|
upstreamUsageKey,
|
||||||
|
|||||||
@@ -116,10 +116,10 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Something went wrong! Could not fetch results.',
|
defaultMessage: 'Something went wrong! Could not fetch results.',
|
||||||
description: 'Generic error message displayed when fetching link data fails.',
|
description: 'Generic error message displayed when fetching link data fails.',
|
||||||
},
|
},
|
||||||
olderVersionPreviewAlert: {
|
unitsUpdatesWarning: {
|
||||||
id: 'course-authoring.course-libraries.reviw-tab.preview.old-version-alert',
|
id: 'course-authoring.course-libraries.home-tab.warning.units',
|
||||||
defaultMessage: 'The old version preview is the previous library version',
|
defaultMessage: 'Currently this page only tracks component updates. To check for unit updates, go to your Course Outline.',
|
||||||
description: 'Alert message stating that older version in preview is of library block',
|
description: 'Warning message shown in library sync page about units updates.',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ const CourseOutline = ({ courseId }) => {
|
|||||||
section,
|
section,
|
||||||
section.childInfo.children,
|
section.childInfo.children,
|
||||||
)}
|
)}
|
||||||
|
isSectionsExpanded={isSectionsExpanded}
|
||||||
isSelfPaced={statusBarData.isSelfPaced}
|
isSelfPaced={statusBarData.isSelfPaced}
|
||||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||||
savingStatus={savingStatus}
|
savingStatus={savingStatus}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { cloneDeep } from 'lodash';
|
|||||||
import { closestCorners } from '@dnd-kit/core';
|
import { closestCorners } from '@dnd-kit/core';
|
||||||
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import {
|
import {
|
||||||
getCourseBestPracticesApiUrl,
|
getCourseBestPracticesApiUrl,
|
||||||
getCourseLaunchApiUrl,
|
getCourseLaunchApiUrl,
|
||||||
@@ -97,12 +96,6 @@ jest.mock('./data/api', () => ({
|
|||||||
getTagsCount: () => jest.fn().mockResolvedValue({}),
|
getTagsCount: () => jest.fn().mockResolvedValue({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../studio-home/hooks', () => ({
|
|
||||||
useStudioHome: () => ({
|
|
||||||
librariesV2Enabled: true,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock ComponentPicker to call onComponentSelected on click
|
// Mock ComponentPicker to call onComponentSelected on click
|
||||||
jest.mock('../library-authoring/component-picker', () => ({
|
jest.mock('../library-authoring/component-picker', () => ({
|
||||||
ComponentPicker: (props) => {
|
ComponentPicker: (props) => {
|
||||||
@@ -160,7 +153,9 @@ describe('<CourseOutline />', () => {
|
|||||||
pathname: mockPathname,
|
pathname: mockPathname,
|
||||||
});
|
});
|
||||||
|
|
||||||
store = initializeStore();
|
store = initializeStore({
|
||||||
|
studioHome: { studioHomeData: { librariesV2Enabled: true } },
|
||||||
|
});
|
||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||||
@@ -179,6 +174,10 @@ describe('<CourseOutline />', () => {
|
|||||||
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
|
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('render CourseOutline component correctly', async () => {
|
it('render CourseOutline component correctly', async () => {
|
||||||
const { getByText } = render(<RootWrapper />);
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
|
||||||
@@ -289,13 +288,15 @@ describe('<CourseOutline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('check that new section list is saved when dragged', async () => {
|
it('check that new section list is saved when dragged', async () => {
|
||||||
const { findAllByRole } = render(<RootWrapper />);
|
const { findAllByRole, findByTestId } = render(<RootWrapper />);
|
||||||
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
|
const expandAllButton = await findByTestId('expand-collapse-all-button');
|
||||||
|
fireEvent.click(expandAllButton);
|
||||||
|
const [section] = store.getState().courseOutline.sectionsList;
|
||||||
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||||
const draggableButton = sectionsDraggers[6];
|
const draggableButton = sectionsDraggers[1];
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPut(getCourseBlockApiUrl(courseBlockId))
|
.onPut(getCourseBlockApiUrl(section.id))
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
|
|
||||||
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
||||||
@@ -314,13 +315,15 @@ describe('<CourseOutline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('check section list is restored to original order when API call fails', async () => {
|
it('check section list is restored to original order when API call fails', async () => {
|
||||||
const { findAllByRole } = render(<RootWrapper />);
|
const { findAllByRole, findByTestId } = render(<RootWrapper />);
|
||||||
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
|
const expandAllButton = await findByTestId('expand-collapse-all-button');
|
||||||
|
fireEvent.click(expandAllButton);
|
||||||
|
const [section] = store.getState().courseOutline.sectionsList;
|
||||||
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||||
const draggableButton = sectionsDraggers[6];
|
const draggableButton = sectionsDraggers[1];
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPut(getCourseBlockApiUrl(courseBlockId))
|
.onPut(getCourseBlockApiUrl(section.id))
|
||||||
.reply(500);
|
.reply(500);
|
||||||
|
|
||||||
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
||||||
@@ -395,8 +398,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const { findAllByTestId } = render(<RootWrapper />);
|
const { findAllByTestId } = render(<RootWrapper />);
|
||||||
const [sectionElement] = await findAllByTestId('section-card');
|
const [sectionElement] = await findAllByTestId('section-card');
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const units = await within(subsectionElement).findAllByTestId('unit-card');
|
const units = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
expect(units.length).toBe(1);
|
expect(units.length).toBe(1);
|
||||||
|
|
||||||
@@ -421,8 +422,6 @@ describe('<CourseOutline />', () => {
|
|||||||
render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
const [sectionElement] = await screen.findAllByTestId('section-card');
|
const [sectionElement] = await screen.findAllByTestId('section-card');
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const units = await within(subsectionElement).findAllByTestId('unit-card');
|
const units = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
expect(units.length).toBe(1);
|
expect(units.length).toBe(1);
|
||||||
|
|
||||||
@@ -646,8 +645,6 @@ describe('<CourseOutline />', () => {
|
|||||||
await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection');
|
await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection');
|
||||||
|
|
||||||
// check unit
|
// check unit
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const [unit] = subsection.childInfo.children;
|
const [unit] = subsection.childInfo.children;
|
||||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit');
|
await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit');
|
||||||
@@ -660,8 +657,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [sectionElement] = await screen.findAllByTestId('section-card');
|
const [sectionElement] = await screen.findAllByTestId('section-card');
|
||||||
const [subsection] = section.childInfo.children;
|
const [subsection] = section.childInfo.children;
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const [unit] = subsection.childInfo.children;
|
const [unit] = subsection.childInfo.children;
|
||||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -700,8 +695,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [sectionElement] = await findAllByTestId('section-card');
|
const [sectionElement] = await findAllByTestId('section-card');
|
||||||
const [subsection] = section.childInfo.children;
|
const [subsection] = section.childInfo.children;
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const [unit] = subsection.childInfo.children;
|
const [unit] = subsection.childInfo.children;
|
||||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -771,8 +764,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [sectionElement] = await findAllByTestId('section-card');
|
const [sectionElement] = await findAllByTestId('section-card');
|
||||||
const [subsection] = section.childInfo.children;
|
const [subsection] = section.childInfo.children;
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const [unit] = subsection.childInfo.children;
|
const [unit] = subsection.childInfo.children;
|
||||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -1481,8 +1472,6 @@ describe('<CourseOutline />', () => {
|
|||||||
|
|
||||||
const [firstSection] = await findAllByTestId('section-card');
|
const [firstSection] = await findAllByTestId('section-card');
|
||||||
const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card');
|
const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card');
|
||||||
const subsectionExpandButton = await within(firstSubsection).getByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(subsectionExpandButton);
|
|
||||||
const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card');
|
const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card');
|
||||||
const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button');
|
const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button');
|
||||||
|
|
||||||
@@ -1842,8 +1831,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [, sectionElement] = await findAllByTestId('section-card');
|
const [, sectionElement] = await findAllByTestId('section-card');
|
||||||
const [, subsection] = section.childInfo.children;
|
const [, subsection] = section.childInfo.children;
|
||||||
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await act(async () => fireEvent.click(expandBtn));
|
|
||||||
const [, secondUnit] = subsection.childInfo.children;
|
const [, secondUnit] = subsection.childInfo.children;
|
||||||
const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -1883,8 +1870,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [, sectionElement] = await findAllByTestId('section-card');
|
const [, sectionElement] = await findAllByTestId('section-card');
|
||||||
const [firstSubsection, subsection] = section.childInfo.children;
|
const [firstSubsection, subsection] = section.childInfo.children;
|
||||||
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await act(async () => fireEvent.click(expandBtn));
|
|
||||||
const [unit] = subsection.childInfo.children;
|
const [unit] = subsection.childInfo.children;
|
||||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -1920,8 +1905,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [subsection] = secondSection.childInfo.children;
|
const [subsection] = secondSection.childInfo.children;
|
||||||
const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1];
|
const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1];
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await act(async () => fireEvent.click(expandBtn));
|
|
||||||
const [unit] = subsection.childInfo.children;
|
const [unit] = subsection.childInfo.children;
|
||||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -1966,8 +1949,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [, sectionElement] = await findAllByTestId('section-card');
|
const [, sectionElement] = await findAllByTestId('section-card');
|
||||||
const [firstSubsection, subsection] = section.childInfo.children;
|
const [firstSubsection, subsection] = section.childInfo.children;
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await act(async () => fireEvent.click(expandBtn));
|
|
||||||
const lastUnitIdx = firstSubsection.childInfo.children.length - 1;
|
const lastUnitIdx = firstSubsection.childInfo.children.length - 1;
|
||||||
const unit = firstSubsection.childInfo.children[lastUnitIdx];
|
const unit = firstSubsection.childInfo.children[lastUnitIdx];
|
||||||
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
|
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
|
||||||
@@ -2005,8 +1986,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex];
|
const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex];
|
||||||
const thirdSectionFirstSubsection = thirdSection.childInfo.children[0];
|
const thirdSectionFirstSubsection = thirdSection.childInfo.children[0];
|
||||||
const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex];
|
const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex];
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await act(async () => fireEvent.click(expandBtn));
|
|
||||||
const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1;
|
const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1;
|
||||||
const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx];
|
const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx];
|
||||||
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
|
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
|
||||||
@@ -2051,8 +2030,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const sections = await findAllByTestId('section-card');
|
const sections = await findAllByTestId('section-card');
|
||||||
const [sectionElement] = sections;
|
const [sectionElement] = sections;
|
||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await act(async () => fireEvent.click(expandBtn));
|
|
||||||
// get first and only unit in the subsection
|
// get first and only unit in the subsection
|
||||||
const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -2072,8 +2049,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const lastSection = sections[sections.length - 1];
|
const lastSection = sections[sections.length - 1];
|
||||||
// it has only one subsection
|
// it has only one subsection
|
||||||
const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card');
|
const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card');
|
||||||
const lastExpandBtn = await within(lastSubsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await act(async () => fireEvent.click(lastExpandBtn));
|
|
||||||
// get last and the only unit in the subsection
|
// get last and the only unit in the subsection
|
||||||
const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card');
|
const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
@@ -2094,6 +2069,9 @@ describe('<CourseOutline />', () => {
|
|||||||
const { findAllByTestId } = render(<RootWrapper />);
|
const { findAllByTestId } = render(<RootWrapper />);
|
||||||
|
|
||||||
const [sectionElement] = await findAllByTestId('section-card');
|
const [sectionElement] = await findAllByTestId('section-card');
|
||||||
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
|
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
||||||
|
fireEvent.click(expandBtn);
|
||||||
const [section] = store.getState().courseOutline.sectionsList;
|
const [section] = store.getState().courseOutline.sectionsList;
|
||||||
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
||||||
const draggableButton = subsectionsDraggers[1];
|
const draggableButton = subsectionsDraggers[1];
|
||||||
@@ -2125,6 +2103,9 @@ describe('<CourseOutline />', () => {
|
|||||||
const { findAllByTestId } = render(<RootWrapper />);
|
const { findAllByTestId } = render(<RootWrapper />);
|
||||||
|
|
||||||
const [sectionElement] = await findAllByTestId('section-card');
|
const [sectionElement] = await findAllByTestId('section-card');
|
||||||
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
|
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
||||||
|
fireEvent.click(expandBtn);
|
||||||
const [section] = store.getState().courseOutline.sectionsList;
|
const [section] = store.getState().courseOutline.sectionsList;
|
||||||
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
||||||
const draggableButton = subsectionsDraggers[1];
|
const draggableButton = subsectionsDraggers[1];
|
||||||
@@ -2154,8 +2135,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const section = store.getState().courseOutline.sectionsList[2];
|
const section = store.getState().courseOutline.sectionsList[2];
|
||||||
const [subsection] = section.childInfo.children;
|
const [subsection] = section.childInfo.children;
|
||||||
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
|
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
|
||||||
const draggableButton = unitDraggers[1];
|
const draggableButton = unitDraggers[1];
|
||||||
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
|
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
|
||||||
@@ -2190,8 +2169,6 @@ describe('<CourseOutline />', () => {
|
|||||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const section = store.getState().courseOutline.sectionsList[2];
|
const section = store.getState().courseOutline.sectionsList[2];
|
||||||
const [subsection] = section.childInfo.children;
|
const [subsection] = section.childInfo.children;
|
||||||
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
|
|
||||||
fireEvent.click(expandBtn);
|
|
||||||
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
|
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
|
||||||
const draggableButton = unitDraggers[1];
|
const draggableButton = unitDraggers[1];
|
||||||
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
|
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
|
||||||
@@ -2229,8 +2206,6 @@ describe('<CourseOutline />', () => {
|
|||||||
.onGet(getXBlockApiUrl(section.id))
|
.onGet(getXBlockApiUrl(section.id))
|
||||||
.reply(200, courseSectionMock);
|
.reply(200, courseSectionMock);
|
||||||
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
|
||||||
await userEvent.click(expandBtn);
|
|
||||||
const [unit] = subsection.childInfo.children;
|
const [unit] = subsection.childInfo.children;
|
||||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
Hyperlink,
|
Hyperlink,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
IconButtonWithTooltip,
|
||||||
useToggle,
|
useToggle,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import {
|
import {
|
||||||
@@ -133,19 +135,24 @@ const CardHeader = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{titleComponent}
|
{titleComponent}
|
||||||
{readyToSync && (
|
<IconButtonWithTooltip
|
||||||
<IconButton
|
className={classNames(
|
||||||
className="item-card-button-icon"
|
'item-card-button-icon',
|
||||||
data-testid={`${namePrefix}-sync-button`}
|
{
|
||||||
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
|
'item-card-button-icon-disabled': isDisabledEditField,
|
||||||
iconAs={SyncIcon}
|
},
|
||||||
onClick={onClickSync}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
className="item-card-button-icon"
|
|
||||||
data-testid={`${namePrefix}-edit-button`}
|
data-testid={`${namePrefix}-edit-button`}
|
||||||
alt={intl.formatMessage(messages.altButtonEdit)}
|
alt={intl.formatMessage(
|
||||||
|
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
|
||||||
|
)}
|
||||||
|
tooltipContent={(
|
||||||
|
<div>
|
||||||
|
{intl.formatMessage(
|
||||||
|
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
iconAs={EditIcon}
|
iconAs={EditIcon}
|
||||||
onClick={onClickEdit}
|
onClick={onClickEdit}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -161,6 +168,15 @@ const CardHeader = ({
|
|||||||
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
|
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
|
||||||
)}
|
)}
|
||||||
{extraActionsComponent}
|
{extraActionsComponent}
|
||||||
|
{readyToSync && (
|
||||||
|
<IconButtonWithTooltip
|
||||||
|
data-testid={`${namePrefix}-sync-button`}
|
||||||
|
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
|
||||||
|
iconAs={SyncIcon}
|
||||||
|
tooltipContent={<div>{intl.formatMessage(messages.readyToSyncButtonAlt)}</div>}
|
||||||
|
onClick={onClickSync}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
|
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
|
||||||
<Dropdown.Toggle
|
<Dropdown.Toggle
|
||||||
className="item-card-header__menu"
|
className="item-card-header__menu"
|
||||||
|
|||||||
@@ -25,6 +25,12 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
.item-card-button-icon {
|
.item-card-button-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
|
&.item-card-button-icon-disabled {
|
||||||
|
pointer-events: all;
|
||||||
|
opacity: .5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.course-outline.card.status-badge.draft-unpublished-changes',
|
id: 'course-authoring.course-outline.card.status-badge.draft-unpublished-changes',
|
||||||
defaultMessage: 'Draft (Unpublished changes)',
|
defaultMessage: 'Draft (Unpublished changes)',
|
||||||
},
|
},
|
||||||
altButtonEdit: {
|
altButtonRename: {
|
||||||
id: 'course-authoring.course-outline.card.button.edit.alt',
|
id: 'course-authoring.course-outline.card.button.edit.alt',
|
||||||
defaultMessage: 'Edit',
|
defaultMessage: 'Rename',
|
||||||
},
|
},
|
||||||
menuPublish: {
|
menuPublish: {
|
||||||
id: 'course-authoring.course-outline.card.menu.publish',
|
id: 'course-authoring.course-outline.card.menu.publish',
|
||||||
@@ -82,6 +82,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Update available - click to sync',
|
defaultMessage: 'Update available - click to sync',
|
||||||
description: 'Alt text for the sync icon button.',
|
description: 'Alt text for the sync icon button.',
|
||||||
},
|
},
|
||||||
|
cannotEditTooltip: {
|
||||||
|
id: 'course-authoring.course-outline.card.button.edit.disable.tooltip',
|
||||||
|
defaultMessage: 'This object was added from a library, so it cannot be edited.',
|
||||||
|
description: 'Tooltip text of button when the object was added from a library.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ const HeaderNavigations = ({
|
|||||||
{hasSections && (
|
{hasSections && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
|
id="expand-collapse-all-button"
|
||||||
|
data-testid="expand-collapse-all-button"
|
||||||
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
|
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
|
||||||
onClick={handleExpandAll}
|
onClick={handleExpandAll}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, {
|
|||||||
useContext, useEffect, useState, useRef, useCallback,
|
useContext, useEffect, useState, useRef, useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, StandardModal, useToggle } from '@openedx/paragon';
|
import { Button, StandardModal, useToggle } from '@openedx/paragon';
|
||||||
@@ -25,12 +25,13 @@ import messages from './messages';
|
|||||||
import { ComponentPicker } from '../../library-authoring';
|
import { ComponentPicker } from '../../library-authoring';
|
||||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||||
import { ContainerType } from '../../generic/key-utils';
|
import { ContainerType } from '../../generic/key-utils';
|
||||||
import { useStudioHome } from '../../studio-home/hooks';
|
|
||||||
import { ContentType } from '../../library-authoring/routes';
|
import { ContentType } from '../../library-authoring/routes';
|
||||||
|
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
||||||
|
|
||||||
const SubsectionCard = ({
|
const SubsectionCard = ({
|
||||||
section,
|
section,
|
||||||
subsection,
|
subsection,
|
||||||
|
isSectionsExpanded,
|
||||||
isSelfPaced,
|
isSelfPaced,
|
||||||
isCustomRelativeDatesActive,
|
isCustomRelativeDatesActive,
|
||||||
children,
|
children,
|
||||||
@@ -57,7 +58,12 @@ const SubsectionCard = ({
|
|||||||
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
||||||
const namePrefix = 'subsection';
|
const namePrefix = 'subsection';
|
||||||
const { sharedClipboardData, showPasteUnit } = useClipboard();
|
const { sharedClipboardData, showPasteUnit } = useClipboard();
|
||||||
const { librariesV2Enabled } = useStudioHome();
|
// WARNING: Do not use "useStudioHome" to get "librariesV2Enabled" flag below,
|
||||||
|
// as it has a useEffect that fetches course waffle flags whenever
|
||||||
|
// location.search is updated. Course search updates location.search when
|
||||||
|
// user types, which will then trigger the useEffect and reload the page.
|
||||||
|
// See https://github.com/openedx/frontend-app-authoring/pull/1938.
|
||||||
|
const { librariesV2Enabled } = useSelector(getStudioHomeData);
|
||||||
const [
|
const [
|
||||||
isAddLibraryUnitModalOpen,
|
isAddLibraryUnitModalOpen,
|
||||||
openAddLibraryUnitModal,
|
openAddLibraryUnitModal,
|
||||||
@@ -93,7 +99,7 @@ const SubsectionCard = ({
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible);
|
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible || isSectionsExpanded);
|
||||||
const subsectionStatus = getItemStatus({
|
const subsectionStatus = getItemStatus({
|
||||||
published,
|
published,
|
||||||
visibilityState,
|
visibilityState,
|
||||||
@@ -101,6 +107,10 @@ const SubsectionCard = ({
|
|||||||
});
|
});
|
||||||
const borderStyle = getItemStatusBorder(subsectionStatus);
|
const borderStyle = getItemStatusBorder(subsectionStatus);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsExpanded(isSectionsExpanded);
|
||||||
|
}, [isSectionsExpanded]);
|
||||||
|
|
||||||
const handleExpandContent = () => {
|
const handleExpandContent = () => {
|
||||||
setIsExpanded((prevState) => !prevState);
|
setIsExpanded((prevState) => !prevState);
|
||||||
};
|
};
|
||||||
@@ -247,7 +257,7 @@ const SubsectionCard = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isExpanded && (
|
{(isExpanded) && (
|
||||||
<div
|
<div
|
||||||
data-testid="subsection-card__units"
|
data-testid="subsection-card__units"
|
||||||
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
|
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
|
||||||
@@ -349,6 +359,7 @@ SubsectionCard.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
isSectionsExpanded: PropTypes.bool.isRequired,
|
||||||
isSelfPaced: PropTypes.bool.isRequired,
|
isSelfPaced: PropTypes.bool.isRequired,
|
||||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||||
onOpenPublishModal: PropTypes.func.isRequired,
|
onOpenPublishModal: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ jest.mock('react-router-dom', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../studio-home/hooks', () => ({
|
jest.mock('react-redux', () => ({
|
||||||
useStudioHome: () => ({
|
...jest.requireActual('react-redux'),
|
||||||
|
useSelector: () => ({
|
||||||
librariesV2Enabled: true,
|
librariesV2Enabled: true,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -229,12 +229,14 @@ const UnitCard = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
<PreviewLibraryXBlockChanges
|
{blockSyncData && (
|
||||||
blockData={blockSyncData}
|
<PreviewLibraryXBlockChanges
|
||||||
isModalOpen={isSyncModalOpen}
|
blockData={blockSyncData}
|
||||||
closeModal={closeSyncModal}
|
isModalOpen={isSyncModalOpen}
|
||||||
postChange={handleOnPostChangeSync}
|
closeModal={closeSyncModal}
|
||||||
/>
|
postChange={handleOnPostChangeSync}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ describe('<UnitCard />', () => {
|
|||||||
|
|
||||||
// Should open compare preview modal
|
// Should open compare preview modal
|
||||||
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
|
||||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
expect(screen.getByText('Preview not available for unit changes at this time')).toBeInTheDocument();
|
||||||
|
|
||||||
// Click on accept changes
|
// Click on accept changes
|
||||||
const acceptChangesButton = screen.getByText(/accept changes/i);
|
const acceptChangesButton = screen.getByText(/accept changes/i);
|
||||||
@@ -196,7 +196,7 @@ describe('<UnitCard />', () => {
|
|||||||
|
|
||||||
// Should open compare preview modal
|
// Should open compare preview modal
|
||||||
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
|
||||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
expect(screen.getByText('Preview not available for unit changes at this time')).toBeInTheDocument();
|
||||||
|
|
||||||
// Click on ignore changes
|
// Click on ignore changes
|
||||||
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
|
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Container, Layout, Stack, Button, TransitionReplace,
|
Alert, Container, Layout, Button, TransitionReplace,
|
||||||
Alert,
|
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -27,8 +26,6 @@ import AddComponent from './add-component/AddComponent';
|
|||||||
import HeaderTitle from './header-title/HeaderTitle';
|
import HeaderTitle from './header-title/HeaderTitle';
|
||||||
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
||||||
import Sequence from './course-sequence';
|
import Sequence from './course-sequence';
|
||||||
import Sidebar from './sidebar';
|
|
||||||
import SplitTestSidebarInfo from './sidebar/SplitTestSidebarInfo';
|
|
||||||
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
|
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { PasteNotificationAlert } from './clipboard';
|
import { PasteNotificationAlert } from './clipboard';
|
||||||
@@ -44,6 +41,7 @@ const CourseUnit = ({ courseId }) => {
|
|||||||
courseUnit,
|
courseUnit,
|
||||||
isLoading,
|
isLoading,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
|
courseUnitLoadingStatus,
|
||||||
unitTitle,
|
unitTitle,
|
||||||
unitCategory,
|
unitCategory,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -140,7 +138,7 @@ const CourseUnit = ({ courseId }) => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</TransitionReplace>
|
</TransitionReplace>
|
||||||
{courseUnit.upstreamInfo.upstreamLink && (
|
{courseUnit.upstreamInfo?.upstreamLink && (
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
title={intl.formatMessage(
|
title={intl.formatMessage(
|
||||||
messages.alertLibraryUnitReadOnlyText,
|
messages.alertLibraryUnitReadOnlyText,
|
||||||
@@ -213,6 +211,7 @@ const CourseUnit = ({ courseId }) => {
|
|||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
blockId={blockId}
|
blockId={blockId}
|
||||||
isUnitVerticalType={isUnitVerticalType}
|
isUnitVerticalType={isUnitVerticalType}
|
||||||
|
courseUnitLoadingStatus={courseUnitLoadingStatus}
|
||||||
unitXBlockActions={unitXBlockActions}
|
unitXBlockActions={unitXBlockActions}
|
||||||
courseVerticalChildren={courseVerticalChildren.children}
|
courseVerticalChildren={courseVerticalChildren.children}
|
||||||
handleConfigureSubmit={handleConfigureSubmit}
|
handleConfigureSubmit={handleConfigureSubmit}
|
||||||
@@ -244,22 +243,15 @@ const CourseUnit = ({ courseId }) => {
|
|||||||
<IframePreviewLibraryXBlockChanges />
|
<IframePreviewLibraryXBlockChanges />
|
||||||
</Layout.Element>
|
</Layout.Element>
|
||||||
<Layout.Element>
|
<Layout.Element>
|
||||||
<Stack gap={3}>
|
<CourseAuthoringUnitSidebarSlot
|
||||||
{isUnitVerticalType && (
|
courseId={courseId}
|
||||||
<CourseAuthoringUnitSidebarSlot
|
blockId={blockId}
|
||||||
courseId={courseId}
|
unitTitle={unitTitle}
|
||||||
blockId={blockId}
|
xBlocks={courseVerticalChildren.children}
|
||||||
unitTitle={unitTitle}
|
readOnly={readOnly}
|
||||||
xBlocks={courseVerticalChildren.children}
|
isUnitVerticalType={isUnitVerticalType}
|
||||||
readOnly={readOnly}
|
isSplitTestType={isSplitTestType}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isSplitTestType && (
|
|
||||||
<Sidebar data-testid="course-split-test-sidebar">
|
|
||||||
<SplitTestSidebarInfo />
|
|
||||||
</Sidebar>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Layout.Element>
|
</Layout.Element>
|
||||||
</Layout>
|
</Layout>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { cloneDeep, set } from 'lodash';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getCourseSectionVerticalApiUrl,
|
getCourseSectionVerticalApiUrl,
|
||||||
getCourseUnitApiUrl,
|
|
||||||
getCourseVerticalChildrenApiUrl,
|
getCourseVerticalChildrenApiUrl,
|
||||||
getCourseOutlineInfoUrl,
|
getCourseOutlineInfoUrl,
|
||||||
getXBlockBaseApiUrl,
|
getXBlockBaseApiUrl,
|
||||||
@@ -28,7 +27,6 @@ import {
|
|||||||
deleteUnitItemQuery,
|
deleteUnitItemQuery,
|
||||||
editCourseUnitVisibilityAndData,
|
editCourseUnitVisibilityAndData,
|
||||||
fetchCourseSectionVerticalData,
|
fetchCourseSectionVerticalData,
|
||||||
fetchCourseUnitQuery,
|
|
||||||
fetchCourseVerticalChildrenData,
|
fetchCourseVerticalChildrenData,
|
||||||
getCourseOutlineInfoQuery,
|
getCourseOutlineInfoQuery,
|
||||||
patchUnitItemQuery,
|
patchUnitItemQuery,
|
||||||
@@ -37,13 +35,12 @@ import initializeStore from '../store';
|
|||||||
import {
|
import {
|
||||||
courseCreateXblockMock,
|
courseCreateXblockMock,
|
||||||
courseSectionVerticalMock,
|
courseSectionVerticalMock,
|
||||||
courseUnitIndexMock,
|
|
||||||
courseUnitMock,
|
courseUnitMock,
|
||||||
courseVerticalChildrenMock,
|
courseVerticalChildrenMock,
|
||||||
clipboardMockResponse,
|
clipboardMockResponse,
|
||||||
courseOutlineInfoMock,
|
courseOutlineInfoMock,
|
||||||
} from './__mocks__';
|
} from './__mocks__';
|
||||||
import { clipboardUnit, clipboardXBlock } from '../__mocks__';
|
import { clipboardUnit } from '../__mocks__';
|
||||||
import { executeThunk } from '../utils';
|
import { executeThunk } from '../utils';
|
||||||
import { IFRAME_FEATURE_POLICY } from '../constants';
|
import { IFRAME_FEATURE_POLICY } from '../constants';
|
||||||
import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
|
import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
|
||||||
@@ -65,13 +62,15 @@ import xblockContainerIframeMessages from './xblock-container-iframe/messages';
|
|||||||
import headerNavigationsMessages from './header-navigations/messages';
|
import headerNavigationsMessages from './header-navigations/messages';
|
||||||
import sidebarMessages from './sidebar/messages';
|
import sidebarMessages from './sidebar/messages';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import * as selectors from '../data/selectors';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
let queryClient;
|
let queryClient;
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
const blockId = '567890';
|
const blockId = '567890';
|
||||||
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
|
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5';
|
||||||
|
const unitDisplayName = courseSectionVerticalMock.xblock_info.display_name;
|
||||||
const mockedUsedNavigate = jest.fn();
|
const mockedUsedNavigate = jest.fn();
|
||||||
const userName = 'openedx';
|
const userName = 'openedx';
|
||||||
const handleConfigureSubmitMock = jest.fn();
|
const handleConfigureSubmitMock = jest.fn();
|
||||||
@@ -89,7 +88,7 @@ const postXBlockBody = {
|
|||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
useParams: () => ({ blockId }),
|
useParams: () => ({ blockId, sequenceId }),
|
||||||
useNavigate: () => mockedUsedNavigate,
|
useNavigate: () => mockedUsedNavigate,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -145,14 +144,10 @@ describe('<CourseUnit />', () => {
|
|||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getClipboardUrl())
|
.onGet(getClipboardUrl())
|
||||||
.reply(200, clipboardUnit);
|
.reply(200, clipboardUnit);
|
||||||
axiosMock
|
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
|
||||||
.reply(200, courseUnitIndexMock);
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseSectionVerticalMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId, courseId), store.dispatch);
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||||
.reply(200, courseVerticalChildrenMock);
|
.reply(200, courseVerticalChildrenMock);
|
||||||
@@ -166,27 +161,27 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('render CourseUnit component correctly', async () => {
|
it('render CourseUnit component correctly', async () => {
|
||||||
const { getByText, getByRole, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the course unit iframe with correct attributes', async () => {
|
it('renders the course unit iframe with correct attributes', async () => {
|
||||||
const { getByTitle } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
|
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
|
||||||
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||||
expect(iframe).toHaveAttribute('style', 'height: 0px;');
|
expect(iframe).toHaveAttribute('style', 'height: 0px;');
|
||||||
@@ -210,27 +205,27 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays an error alert when a studioAjaxError message is received', async () => {
|
it('displays an error alert when a studioAjaxError message is received', async () => {
|
||||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(xblocksIframe).toBeInTheDocument();
|
expect(xblocksIframe).toBeInTheDocument();
|
||||||
simulatePostMessageEvent(messageTypes.studioAjaxError, {
|
simulatePostMessageEvent(messageTypes.studioAjaxError, {
|
||||||
error: 'Some error text...',
|
error: 'Some error text...',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(getByTestId('saving-error-alert')).toBeInTheDocument();
|
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
|
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
|
||||||
const { getByTitle } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(xblocksIframe).toBeInTheDocument();
|
expect(xblocksIframe).toBeInTheDocument();
|
||||||
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
|
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
|
||||||
|
|
||||||
const legacyXBlockEditModalIframe = getByTitle(
|
const legacyXBlockEditModalIframe = screen.getByTitle(
|
||||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||||
);
|
);
|
||||||
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
|
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
|
||||||
@@ -248,14 +243,14 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
|
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
|
||||||
const { getByTitle, queryByTitle } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(xblocksIframe).toBeInTheDocument();
|
expect(xblocksIframe).toBeInTheDocument();
|
||||||
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
|
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
|
||||||
|
|
||||||
const legacyXBlockEditModalIframe = queryByTitle(
|
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||||
);
|
);
|
||||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||||
@@ -263,29 +258,32 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
|
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
|
||||||
const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(xblocksIframe).toBeInTheDocument();
|
expect(xblocksIframe).toBeInTheDocument();
|
||||||
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
|
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
|
||||||
|
|
||||||
const legacyXBlockEditModalIframe = queryByTitle(
|
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||||
);
|
);
|
||||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
has_changes: true,
|
xblock_info: {
|
||||||
published_by: userName,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
has_changes: true,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||||
expect(
|
expect(
|
||||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -304,24 +302,27 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates course unit sidebar after receiving refreshPositions message', async () => {
|
it('updates course unit sidebar after receiving refreshPositions message', async () => {
|
||||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(xblocksIframe).toBeInTheDocument();
|
expect(xblocksIframe).toBeInTheDocument();
|
||||||
simulatePostMessageEvent(messageTypes.refreshPositions);
|
simulatePostMessageEvent(messageTypes.refreshPositions);
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
has_changes: true,
|
xblock_info: {
|
||||||
published_by: userName,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
has_changes: true,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||||
expect(
|
expect(
|
||||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -340,12 +341,10 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
|
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByTitle, getByText, queryByRole, getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toHaveAttribute(
|
expect(iframe).toHaveAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||||
@@ -356,10 +355,10 @@ describe('<CourseUnit />', () => {
|
|||||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
|
expect(screen.getByText(/Delete this component?/i)).toBeInTheDocument();
|
||||||
expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
|
expect(screen.getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
|
||||||
|
|
||||||
const dialog = getByRole('dialog');
|
const dialog = screen.getByRole('dialog');
|
||||||
expect(dialog).toBeInTheDocument();
|
expect(dialog).toBeInTheDocument();
|
||||||
|
|
||||||
// Find the Cancel and Delete buttons within the iframe by their specific classes
|
// Find the Cancel and Delete buttons within the iframe by their specific classes
|
||||||
@@ -372,7 +371,7 @@ describe('<CourseUnit />', () => {
|
|||||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
userEvent.click(deleteButton);
|
userEvent.click(deleteButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -382,30 +381,36 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
xblock_info: {
|
||||||
has_changes: false,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
published_by: userName,
|
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||||
|
has_changes: false,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// check if the sidebar status is Published and Live
|
// check if the sidebar status is Published and Live
|
||||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.publishLastPublished.defaultMessage
|
sidebarMessages.publishLastPublished.defaultMessage
|
||||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||||
.replace('{publishedBy}', userName),
|
.replace('{publishedBy}', userName),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
|
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
|
axiosMock
|
||||||
|
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||||
|
.reply(200, courseSectionVerticalMock);
|
||||||
await executeThunk(deleteUnitItemQuery(
|
await executeThunk(deleteUnitItemQuery(
|
||||||
courseId,
|
courseId,
|
||||||
courseVerticalChildrenMock.children[0].block_id,
|
courseVerticalChildrenMock.children[0].block_id,
|
||||||
@@ -426,43 +431,41 @@ describe('<CourseUnit />', () => {
|
|||||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toHaveAttribute(
|
expect(iframe).toHaveAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||||
);
|
);
|
||||||
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
|
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
|
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByTitle, getByRole, getByText, queryByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
|
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
|
||||||
id: courseVerticalChildrenMock.children[0].block_id,
|
id: courseVerticalChildrenMock.children[0].block_id,
|
||||||
@@ -478,8 +481,14 @@ describe('<CourseUnit />', () => {
|
|||||||
const updatedCourseVerticalChildren = [
|
const updatedCourseVerticalChildren = [
|
||||||
...courseVerticalChildrenMock.children,
|
...courseVerticalChildrenMock.children,
|
||||||
{
|
{
|
||||||
...courseVerticalChildrenMock.children[0],
|
|
||||||
name: 'New Cloned XBlock',
|
name: 'New Cloned XBlock',
|
||||||
|
block_id: '1234567890',
|
||||||
|
block_type: 'drag-and-drop-v2',
|
||||||
|
user_partition_info: {
|
||||||
|
selectable_partitions: [],
|
||||||
|
selected_partition_index: -1,
|
||||||
|
selected_groups_label: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -491,9 +500,9 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||||
|
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toHaveAttribute(
|
expect(iframe).toHaveAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||||
@@ -511,34 +520,37 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
xblock_info: {
|
||||||
has_changes: false,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
published_by: userName,
|
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||||
|
has_changes: false,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// check if the sidebar status is Published and Live
|
// check if the sidebar status is Published and Live
|
||||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.publishLastPublished.defaultMessage
|
sidebarMessages.publishLastPublished.defaultMessage
|
||||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||||
.replace('{publishedBy}', userName),
|
.replace('{publishedBy}', userName),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toHaveAttribute(
|
expect(iframe).toHaveAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||||
@@ -546,23 +558,23 @@ describe('<CourseUnit />', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
|
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -570,19 +582,19 @@ describe('<CourseUnit />', () => {
|
|||||||
it('handles CourseUnit header action buttons', async () => {
|
it('handles CourseUnit header action buttons', async () => {
|
||||||
const { open } = window;
|
const { open } = window;
|
||||||
window.open = jest.fn();
|
window.open = jest.fn();
|
||||||
const { getByRole } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
const {
|
const {
|
||||||
draft_preview_link: draftPreviewLink,
|
draft_preview_link: draftPreviewLink,
|
||||||
published_preview_link: publishedPreviewLink,
|
published_preview_link: publishedPreviewLink,
|
||||||
} = courseSectionVerticalMock;
|
} = courseSectionVerticalMock;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
const viewLiveButton = screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
||||||
userEvent.click(viewLiveButton);
|
userEvent.click(viewLiveButton);
|
||||||
expect(window.open).toHaveBeenCalled();
|
expect(window.open).toHaveBeenCalled();
|
||||||
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
|
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
|
||||||
|
|
||||||
const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
const previewButton = screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
||||||
userEvent.click(previewButton);
|
userEvent.click(previewButton);
|
||||||
expect(window.open).toHaveBeenCalled();
|
expect(window.open).toHaveBeenCalled();
|
||||||
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
|
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
|
||||||
@@ -592,12 +604,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('checks courseUnit title changing when edit query is successfully', async () => {
|
it('checks courseUnit title changing when edit query is successfully', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
findByText,
|
|
||||||
queryByRole,
|
|
||||||
getByRole,
|
|
||||||
getByTestId,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
let editTitleButton = null;
|
let editTitleButton = null;
|
||||||
let titleEditField = null;
|
let titleEditField = null;
|
||||||
const newDisplayName = `${unitDisplayName} new`;
|
const newDisplayName = `${unitDisplayName} new`;
|
||||||
@@ -610,12 +617,15 @@ describe('<CourseUnit />', () => {
|
|||||||
}))
|
}))
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
metadata: {
|
xblock_info: {
|
||||||
...courseUnitIndexMock.metadata,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
display_name: newDisplayName,
|
metadata: {
|
||||||
|
...courseSectionVerticalMock.xblock_info.metadata,
|
||||||
|
display_name: newDisplayName,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
axiosMock
|
axiosMock
|
||||||
@@ -633,7 +643,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||||
editTitleButton = within(unitHeaderTitle)
|
editTitleButton = within(unitHeaderTitle)
|
||||||
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||||
titleEditField = within(unitHeaderTitle)
|
titleEditField = within(unitHeaderTitle)
|
||||||
@@ -641,7 +651,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
expect(titleEditField).not.toBeInTheDocument();
|
expect(titleEditField).not.toBeInTheDocument();
|
||||||
userEvent.click(editTitleButton);
|
userEvent.click(editTitleButton);
|
||||||
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||||
|
|
||||||
await userEvent.clear(titleEditField);
|
await userEvent.clear(titleEditField);
|
||||||
await userEvent.type(titleEditField, newDisplayName);
|
await userEvent.type(titleEditField, newDisplayName);
|
||||||
@@ -649,9 +659,10 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
expect(titleEditField).toHaveValue(newDisplayName);
|
expect(titleEditField).toHaveValue(newDisplayName);
|
||||||
|
|
||||||
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||||
|
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||||
expect(titleEditField).not.toBeInTheDocument();
|
expect(titleEditField).not.toBeInTheDocument();
|
||||||
expect(await findByText(newDisplayName)).toBeInTheDocument();
|
expect(await screen.findByText(newDisplayName)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('doesn\'t handle creating xblock and displays an error message', async () => {
|
it('doesn\'t handle creating xblock and displays an error message', async () => {
|
||||||
@@ -671,15 +682,14 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handle creating Problem xblock and navigate to editor page', async () => {
|
it('handle creating Problem xblock and showing editor modal', async () => {
|
||||||
const { courseKey, locator } = courseCreateXblockMock;
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
|
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
|
||||||
.reply(200, courseCreateXblockMock);
|
.reply(200, courseCreateXblockMock);
|
||||||
const { getByText, getByRole } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
@@ -688,93 +698,57 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
xblock_info: {
|
||||||
has_changes: false,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
published_by: userName,
|
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||||
|
has_changes: false,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const problemButton = getByRole('button', {
|
const problemButton = screen.getByRole('button', {
|
||||||
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
||||||
|
hidden: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
userEvent.click(problemButton);
|
userEvent.click(problemButton);
|
||||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
|
||||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
|
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
|
||||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handle creating Text xblock and saves scroll position in localStorage', async () => {
|
|
||||||
const { getByText, getByRole } = render(<RootWrapper />);
|
|
||||||
const xblockType = 'text';
|
|
||||||
|
|
||||||
axiosMock
|
|
||||||
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
|
|
||||||
.reply(200, courseCreateXblockMock);
|
|
||||||
|
|
||||||
window.scrollTo(0, 250);
|
|
||||||
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const textButton = screen.getByRole('button', { name: /Text/i });
|
|
||||||
|
|
||||||
expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();
|
|
||||||
|
|
||||||
userEvent.click(textButton);
|
|
||||||
|
|
||||||
const addXBlockDialog = getByRole('dialog');
|
|
||||||
expect(addXBlockDialog).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(getByText(
|
|
||||||
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
|
|
||||||
)).toBeInTheDocument();
|
|
||||||
|
|
||||||
const textRadio = screen.getByRole('radio', { name: /Text/i });
|
|
||||||
userEvent.click(textRadio);
|
|
||||||
expect(textRadio).toBeChecked();
|
|
||||||
|
|
||||||
const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
|
|
||||||
expect(selectBtn).toBeInTheDocument();
|
|
||||||
|
|
||||||
userEvent.click(selectBtn);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
|
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
|
||||||
const { getByRole, getAllByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
let units = null;
|
let units = null;
|
||||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||||
@@ -784,7 +758,7 @@ describe('<CourseUnit />', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
units = getAllByTestId('course-unit-btn');
|
units = screen.getAllByTestId('course-unit-btn');
|
||||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||||
expect(units).toHaveLength(courseUnits.length);
|
expect(units).toHaveLength(courseUnits.length);
|
||||||
});
|
});
|
||||||
@@ -801,8 +775,8 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
const addNewUnitBtn = getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
|
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
|
||||||
units = getAllByTestId('course-unit-btn');
|
units = screen.getAllByTestId('course-unit-btn');
|
||||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||||
|
|
||||||
@@ -814,7 +788,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('the sequence unit is updated after changing the unit header', async () => {
|
it('the sequence unit is updated after changing the unit header', async () => {
|
||||||
const { getAllByTestId, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||||
@@ -831,12 +805,15 @@ describe('<CourseUnit />', () => {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
.reply(200, { dummy: 'value' })
|
.reply(200, { dummy: 'value' })
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
metadata: {
|
xblock_info: {
|
||||||
...courseUnitIndexMock.metadata,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
display_name: newDisplayName,
|
metadata: {
|
||||||
|
...courseSectionVerticalMock.xblock_info.metadata,
|
||||||
|
display_name: newDisplayName,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
@@ -846,7 +823,7 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||||
|
|
||||||
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||||
userEvent.click(editTitleButton);
|
userEvent.click(editTitleButton);
|
||||||
@@ -858,20 +835,21 @@ describe('<CourseUnit />', () => {
|
|||||||
await userEvent.tab();
|
await userEvent.tab();
|
||||||
|
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const units = getAllByTestId('course-unit-btn');
|
const units = screen.getAllByTestId('course-unit-btn');
|
||||||
expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
|
expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles creating Video xblock and navigates to editor page', async () => {
|
it('handles creating Video xblock and showing editor modal using videogalleryflow', async () => {
|
||||||
const { courseKey, locator } = courseCreateXblockMock;
|
const waffleSpy = jest.spyOn(selectors, 'getWaffleFlags').mockReturnValue({ useVideoGalleryFlow: true });
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||||
.reply(200, courseCreateXblockMock);
|
.reply(200, courseCreateXblockMock);
|
||||||
const { getByText, queryByRole, getByRole } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
@@ -880,96 +858,181 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
xblock_info: {
|
||||||
has_changes: false,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
published_by: userName,
|
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||||
|
has_changes: false,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// check if the sidebar status is Published and Live
|
// check if the sidebar status is Published and Live
|
||||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(
|
|
||||||
sidebarMessages.publishLastPublished.defaultMessage
|
|
||||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
|
||||||
.replace('{publishedBy}', userName),
|
|
||||||
)).toBeInTheDocument();
|
|
||||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
const videoButton = getByRole('button', {
|
|
||||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.click(videoButton);
|
|
||||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
|
||||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText(
|
||||||
|
sidebarMessages.publishLastPublished.defaultMessage
|
||||||
|
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||||
|
.replace('{publishedBy}', userName),
|
||||||
|
)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const videoButton = screen.getByRole('button', {
|
||||||
|
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
||||||
|
hidden: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
userEvent.click(videoButton);
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
|
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
|
||||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
.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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles creating Video xblock and showing editor modal', async () => {
|
||||||
|
axiosMock
|
||||||
|
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||||
|
.reply(200, courseCreateXblockMock);
|
||||||
|
render(<RootWrapper />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosMock
|
||||||
|
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||||
|
publish: PUBLISH_TYPES.makePublic,
|
||||||
|
})
|
||||||
|
.reply(200, { dummy: 'value' });
|
||||||
|
axiosMock
|
||||||
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
|
.reply(200, {
|
||||||
|
...courseSectionVerticalMock,
|
||||||
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||||
|
has_changes: false,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// check if the sidebar status is Published and Live
|
||||||
|
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(
|
||||||
|
sidebarMessages.publishLastPublished.defaultMessage
|
||||||
|
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||||
|
.replace('{publishedBy}', userName),
|
||||||
|
)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const videoButton = screen.getByRole('button', {
|
||||||
|
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
||||||
|
hidden: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
userEvent.click(videoButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** TODO -- fix this test.
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
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();
|
||||||
|
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.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(
|
||||||
|
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||||
|
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||||
|
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||||
|
)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(
|
||||||
|
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||||
|
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders course unit details for a draft with unpublished changes', async () => {
|
it('renders course unit details for a draft with unpublished changes', async () => {
|
||||||
const { getByText } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders course unit details in the sidebar', async () => {
|
it('renders course unit details in the sidebar', async () => {
|
||||||
const { getByText } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
|
const courseUnitLocationId = extractCourseUnitId(courseSectionVerticalMock.xblock_info.id);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(courseUnitLocationId)).toBeInTheDocument();
|
expect(screen.getByText(courseUnitLocationId)).toBeInTheDocument();
|
||||||
expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage
|
expect(screen.getByText(sidebarMessages.unitLocationDescription.defaultMessage
|
||||||
.replace('{id}', courseUnitLocationId))).toBeInTheDocument();
|
.replace('{id}', courseUnitLocationId))).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -990,13 +1053,16 @@ describe('<CourseUnit />', () => {
|
|||||||
render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
currently_visible_to_students: false,
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
currently_visible_to_students: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const alert = screen.queryAllByRole('alert').find(
|
const alert = screen.queryAllByRole('alert').find(
|
||||||
@@ -1007,13 +1073,13 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
|
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
|
||||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
let courseUnitSidebar;
|
let courseUnitSidebar;
|
||||||
let draftUnpublishedChangesHeading;
|
let draftUnpublishedChangesHeading;
|
||||||
let visibilityCheckbox;
|
let visibilityCheckbox;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||||
|
|
||||||
draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||||
@@ -1033,11 +1099,14 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
xblock_info: {
|
||||||
has_explicit_staff_lock: true,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||||
|
has_explicit_staff_lock: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
|
||||||
@@ -1050,7 +1119,7 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
userEvent.click(visibilityCheckbox);
|
userEvent.click(visibilityCheckbox);
|
||||||
|
|
||||||
const modalNotification = getByRole('dialog');
|
const modalNotification = screen.getByRole('dialog');
|
||||||
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage });
|
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage });
|
||||||
const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
|
const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
|
||||||
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
|
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
|
||||||
@@ -1070,8 +1139,8 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch);
|
||||||
|
|
||||||
@@ -1082,12 +1151,12 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should publish course unit after click on the "Publish" button', async () => {
|
it('should publish course unit after click on the "Publish" button', async () => {
|
||||||
const { getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
let courseUnitSidebar;
|
let courseUnitSidebar;
|
||||||
let publishBtn;
|
let publishBtn;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||||
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
|
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
|
||||||
expect(publishBtn).toBeInTheDocument();
|
expect(publishBtn).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -1100,12 +1169,15 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
xblock_info: {
|
||||||
has_changes: false,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
published_by: userName,
|
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||||
|
has_changes: false,
|
||||||
|
published_by: userName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||||
@@ -1114,19 +1186,19 @@ describe('<CourseUnit />', () => {
|
|||||||
.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||||
expect(within(courseUnitSidebar).getByText(
|
expect(within(courseUnitSidebar).getByText(
|
||||||
sidebarMessages.publishLastPublished.defaultMessage
|
sidebarMessages.publishLastPublished.defaultMessage
|
||||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||||
.replace('{publishedBy}', userName),
|
.replace('{publishedBy}', userName),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(publishBtn).not.toBeInTheDocument();
|
expect(publishBtn).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should discard changes after click on the "Discard changes" button', async () => {
|
it('should discard changes after click on the "Discard changes" button', async () => {
|
||||||
const { getByTestId, getByRole } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
let courseUnitSidebar;
|
let courseUnitSidebar;
|
||||||
let discardChangesBtn;
|
let discardChangesBtn;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||||
|
|
||||||
const draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
const draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||||
@@ -1136,7 +1208,7 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
userEvent.click(discardChangesBtn);
|
userEvent.click(discardChangesBtn);
|
||||||
|
|
||||||
const modalNotification = getByRole('dialog');
|
const modalNotification = screen.getByRole('dialog');
|
||||||
expect(modalNotification).toBeInTheDocument();
|
expect(modalNotification).toBeInTheDocument();
|
||||||
expect(within(modalNotification)
|
expect(within(modalNotification)
|
||||||
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
|
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
|
||||||
@@ -1156,9 +1228,14 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock, published: true, has_changes: false,
|
...courseSectionVerticalMock,
|
||||||
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
published: true,
|
||||||
|
has_changes: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(editCourseUnitVisibilityAndData(
|
await executeThunk(editCourseUnitVisibilityAndData(
|
||||||
@@ -1173,7 +1250,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
|
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
|
||||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
let courseUnitSidebar;
|
let courseUnitSidebar;
|
||||||
let sidebarVisibilityCheckbox;
|
let sidebarVisibilityCheckbox;
|
||||||
let modalVisibilityCheckbox;
|
let modalVisibilityCheckbox;
|
||||||
@@ -1181,16 +1258,16 @@ describe('<CourseUnit />', () => {
|
|||||||
let restrictAccessSelect;
|
let restrictAccessSelect;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||||
sidebarVisibilityCheckbox = within(courseUnitSidebar)
|
sidebarVisibilityCheckbox = within(courseUnitSidebar)
|
||||||
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
|
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
|
||||||
expect(sidebarVisibilityCheckbox).not.toBeChecked();
|
expect(sidebarVisibilityCheckbox).not.toBeChecked();
|
||||||
|
|
||||||
const headerConfigureBtn = getByRole('button', { name: /settings/i });
|
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i });
|
||||||
expect(headerConfigureBtn).toBeInTheDocument();
|
expect(headerConfigureBtn).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.click(headerConfigureBtn);
|
userEvent.click(headerConfigureBtn);
|
||||||
configureModal = getByTestId('configure-modal');
|
configureModal = screen.getByTestId('configure-modal');
|
||||||
restrictAccessSelect = within(configureModal)
|
restrictAccessSelect = within(configureModal)
|
||||||
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
|
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
|
||||||
expect(within(configureModal)
|
expect(within(configureModal)
|
||||||
@@ -1215,17 +1292,20 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPost(getXBlockBaseApiUrl(courseUnitIndexMock.id), {
|
.onPost(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id), {
|
||||||
publish: null,
|
publish: null,
|
||||||
metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true },
|
metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true },
|
||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.replyOnce(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
xblock_info: {
|
||||||
has_explicit_staff_lock: true,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||||
|
has_explicit_staff_lock: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const modalSaveBtn = within(configureModal)
|
const modalSaveBtn = within(configureModal)
|
||||||
@@ -1246,8 +1326,8 @@ describe('<CourseUnit />', () => {
|
|||||||
...getConfig(),
|
...getConfig(),
|
||||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||||
});
|
});
|
||||||
const { getByText } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
await waitFor(() => { expect(getByText('Unit tags')).toBeInTheDocument(); });
|
await waitFor(() => { expect(screen.getByText('Unit tags')).toBeInTheDocument(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides the Tags sidebar when not enabled', async () => {
|
it('hides the Tags sidebar when not enabled', async () => {
|
||||||
@@ -1255,28 +1335,28 @@ describe('<CourseUnit />', () => {
|
|||||||
...getConfig(),
|
...getConfig(),
|
||||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
|
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
|
||||||
});
|
});
|
||||||
const { queryByText } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
await waitFor(() => { expect(queryByText('Unit tags')).not.toBeInTheDocument(); });
|
await waitFor(() => { expect(screen.queryByText('Unit tags')).not.toBeInTheDocument(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Copy paste functionality', () => {
|
describe('Copy paste functionality', () => {
|
||||||
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
|
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getAllByTestId, getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
enable_copy_paste_units: true,
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
enable_copy_paste_units: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||||
|
|
||||||
let units = null;
|
let units = null;
|
||||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||||
@@ -1287,7 +1367,7 @@ describe('<CourseUnit />', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
units = getAllByTestId('course-unit-btn');
|
units = screen.getAllByTestId('course-unit-btn');
|
||||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||||
expect(units).toHaveLength(courseUnits.length);
|
expect(units).toHaveLength(courseUnits.length);
|
||||||
});
|
});
|
||||||
@@ -1303,7 +1383,7 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
units = getAllByTestId('course-unit-btn');
|
units = screen.getAllByTestId('course-unit-btn');
|
||||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||||
|
|
||||||
@@ -1314,30 +1394,27 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
|
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
|
||||||
const { getByRole, getByTitle } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
simulatePostMessageEvent(messageTypes.copyXBlock, {
|
simulatePostMessageEvent(messageTypes.copyXBlock, {
|
||||||
id: courseVerticalChildrenMock.children[0].block_id,
|
id: courseVerticalChildrenMock.children[0].block_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getClipboardUrl())
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, clipboardXBlock);
|
|
||||||
|
|
||||||
axiosMock
|
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
enable_copy_paste_units: true,
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
enable_copy_paste_units: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||||
userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toHaveAttribute(
|
expect(iframe).toHaveAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||||
@@ -1373,7 +1450,7 @@ describe('<CourseUnit />', () => {
|
|||||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toHaveAttribute(
|
expect(iframe).toHaveAttribute(
|
||||||
'aria-label',
|
'aria-label',
|
||||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||||
@@ -1383,22 +1460,22 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays a notification about new files after pasting a component', async () => {
|
it('displays a notification about new files after pasting a component', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
queryByTestId, getByTestId, getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
enable_copy_paste_units: true,
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
enable_copy_paste_units: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||||
|
|
||||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||||
@@ -1417,7 +1494,7 @@ describe('<CourseUnit />', () => {
|
|||||||
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
||||||
const newFilesAlert = getByTestId('has-new-files-alert');
|
const newFilesAlert = screen.getByTestId('has-new-files-alert');
|
||||||
|
|
||||||
expect(within(newFilesAlert)
|
expect(within(newFilesAlert)
|
||||||
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
|
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
|
||||||
@@ -1431,26 +1508,26 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
|
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
|
||||||
|
|
||||||
expect(queryByTestId('has-new-files-alert')).toBeNull();
|
expect(screen.queryByTestId('has-new-files-alert')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays a notification about conflicting errors after pasting a component', async () => {
|
it('displays a notification about conflicting errors after pasting a component', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
queryByTestId, getByTestId, getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
enable_copy_paste_units: true,
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
enable_copy_paste_units: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||||
|
|
||||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||||
@@ -1471,7 +1548,7 @@ describe('<CourseUnit />', () => {
|
|||||||
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
||||||
const conflictingErrorsAlert = getByTestId('has-conflicting-errors-alert');
|
const conflictingErrorsAlert = screen.getByTestId('has-conflicting-errors-alert');
|
||||||
|
|
||||||
expect(within(conflictingErrorsAlert)
|
expect(within(conflictingErrorsAlert)
|
||||||
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
|
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
|
||||||
@@ -1485,26 +1562,26 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
|
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
|
||||||
|
|
||||||
expect(queryByTestId('has-conflicting-errors-alert')).toBeNull();
|
expect(screen.queryByTestId('has-conflicting-errors-alert')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays a notification about error files after pasting a component', async () => {
|
it('displays a notification about error files after pasting a component', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
queryByTestId, getByTestId, getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
enable_copy_paste_units: true,
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
enable_copy_paste_units: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||||
|
|
||||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||||
@@ -1525,7 +1602,7 @@ describe('<CourseUnit />', () => {
|
|||||||
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
||||||
const errorFilesAlert = getByTestId('has-error-files-alert');
|
const errorFilesAlert = screen.getByTestId('has-error-files-alert');
|
||||||
|
|
||||||
expect(within(errorFilesAlert)
|
expect(within(errorFilesAlert)
|
||||||
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
|
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
|
||||||
@@ -1534,11 +1611,11 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
|
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
|
||||||
|
|
||||||
expect(queryByTestId('has-error-files')).toBeNull();
|
expect(screen.queryByTestId('has-error-files')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide the "Paste component" block if canPasteComponent is false', async () => {
|
it('should hide the "Paste component" block if canPasteComponent is false', async () => {
|
||||||
const { queryByText, queryByRole } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||||
@@ -1549,10 +1626,10 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||||
|
|
||||||
expect(queryByRole('button', {
|
expect(screen.queryByRole('button', {
|
||||||
name: messages.pasteButtonText.defaultMessage,
|
name: messages.pasteButtonText.defaultMessage,
|
||||||
})).not.toBeInTheDocument();
|
})).not.toBeInTheDocument();
|
||||||
expect(queryByText(
|
expect(screen.queryByText(
|
||||||
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
|
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
|
||||||
)).not.toBeInTheDocument();
|
)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -1586,9 +1663,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display "Move Modal" on receive trigger message', async () => {
|
it('should display "Move Modal" on receive trigger message', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
await screen.findByText(unitDisplayName);
|
await screen.findByText(unitDisplayName);
|
||||||
|
|
||||||
@@ -1602,15 +1677,12 @@ describe('<CourseUnit />', () => {
|
|||||||
await screen.findByText(
|
await screen.findByText(
|
||||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||||
);
|
);
|
||||||
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigates to xBlock current unit', async () => {
|
it('should navigates to xBlock current unit', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByText,
|
|
||||||
getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
await screen.findByText(unitDisplayName);
|
await screen.findByText(unitDisplayName);
|
||||||
|
|
||||||
@@ -1626,7 +1698,7 @@ describe('<CourseUnit />', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||||
const currentSectionItemBtn = getByRole('button', {
|
const currentSectionItemBtn = screen.getByRole('button', {
|
||||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||||
});
|
});
|
||||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||||
@@ -1634,7 +1706,7 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const currentSubsection = currentSection.child_info.children[0];
|
const currentSubsection = currentSection.child_info.children[0];
|
||||||
const currentSubsectionItemBtn = getByRole('button', {
|
const currentSubsectionItemBtn = screen.getByRole('button', {
|
||||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||||
});
|
});
|
||||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||||
@@ -1642,7 +1714,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const currentComponentLocationText = getByText(
|
const currentComponentLocationText = screen.getByText(
|
||||||
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
|
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
|
||||||
);
|
);
|
||||||
expect(currentComponentLocationText).toBeInTheDocument();
|
expect(currentComponentLocationText).toBeInTheDocument();
|
||||||
@@ -1650,17 +1722,15 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow move operation and handles it successfully', async () => {
|
it('should allow move operation and handles it successfully', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPatch(postXBlockBaseApiUrl())
|
.onPatch(postXBlockBaseApiUrl())
|
||||||
.reply(200, {});
|
.reply(200, {});
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
|
|
||||||
await screen.findByText(unitDisplayName);
|
await screen.findByText(unitDisplayName);
|
||||||
|
|
||||||
@@ -1676,7 +1746,7 @@ describe('<CourseUnit />', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||||
const currentSectionItemBtn = getByRole('button', {
|
const currentSectionItemBtn = screen.getByRole('button', {
|
||||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||||
});
|
});
|
||||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||||
@@ -1684,7 +1754,7 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
const currentSubsection = currentSection.child_info.children[1];
|
const currentSubsection = currentSection.child_info.children[1];
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const currentSubsectionItemBtn = getByRole('button', {
|
const currentSubsectionItemBtn = screen.getByRole('button', {
|
||||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||||
});
|
});
|
||||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||||
@@ -1693,14 +1763,14 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const currentUnit = currentSubsection.child_info.children[0];
|
const currentUnit = currentSubsection.child_info.children[0];
|
||||||
const currentUnitItemBtn = getByRole('button', {
|
const currentUnitItemBtn = screen.getByRole('button', {
|
||||||
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||||
});
|
});
|
||||||
expect(currentUnitItemBtn).toBeInTheDocument();
|
expect(currentUnitItemBtn).toBeInTheDocument();
|
||||||
userEvent.click(currentUnitItemBtn);
|
userEvent.click(currentUnitItemBtn);
|
||||||
});
|
});
|
||||||
|
|
||||||
const moveModalBtn = getByRole('button', {
|
const moveModalBtn = screen.getByRole('button', {
|
||||||
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
|
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
|
||||||
});
|
});
|
||||||
expect(moveModalBtn).toBeInTheDocument();
|
expect(moveModalBtn).toBeInTheDocument();
|
||||||
@@ -1714,10 +1784,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
|
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
queryByRole,
|
|
||||||
getByText,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPatch(postXBlockBaseApiUrl())
|
.onPatch(postXBlockBaseApiUrl())
|
||||||
@@ -1734,18 +1801,18 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
|
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
|
||||||
|
|
||||||
const dismissButton = queryByRole('button', {
|
const dismissButton = screen.queryByRole('button', {
|
||||||
name: /dismiss/i, hidden: true,
|
name: /dismiss/i, hidden: true,
|
||||||
});
|
});
|
||||||
const undoButton = queryByRole('button', {
|
const undoButton = screen.queryByRole('button', {
|
||||||
name: messages.undoMoveButton.defaultMessage, hidden: true,
|
name: messages.undoMoveButton.defaultMessage, hidden: true,
|
||||||
});
|
});
|
||||||
const newLocationButton = queryByRole('button', {
|
const newLocationButton = screen.queryByRole('button', {
|
||||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
|
expect(screen.getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
|
||||||
expect(dismissButton).toBeInTheDocument();
|
expect(dismissButton).toBeInTheDocument();
|
||||||
expect(undoButton).toBeInTheDocument();
|
expect(undoButton).toBeInTheDocument();
|
||||||
expect(newLocationButton).toBeInTheDocument();
|
expect(newLocationButton).toBeInTheDocument();
|
||||||
@@ -1753,9 +1820,9 @@ describe('<CourseUnit />', () => {
|
|||||||
userEvent.click(undoButton);
|
userEvent.click(undoButton);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
|
expect(screen.getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(getByText(
|
expect(screen.getByText(
|
||||||
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
|
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
|
||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
expect(dismissButton).toBeInTheDocument();
|
expect(dismissButton).toBeInTheDocument();
|
||||||
@@ -1764,9 +1831,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to new location by button click', async () => {
|
it('should navigate to new location by button click', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
queryByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPatch(postXBlockBaseApiUrl())
|
.onPatch(postXBlockBaseApiUrl())
|
||||||
@@ -1781,7 +1846,7 @@ describe('<CourseUnit />', () => {
|
|||||||
callbackFn: requestData.callbackFn,
|
callbackFn: requestData.callbackFn,
|
||||||
}), store.dispatch);
|
}), store.dispatch);
|
||||||
|
|
||||||
const newLocationButton = queryByRole('button', {
|
const newLocationButton = screen.queryByRole('button', {
|
||||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||||
});
|
});
|
||||||
userEvent.click(newLocationButton);
|
userEvent.click(newLocationButton);
|
||||||
@@ -1794,16 +1859,14 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
describe('XBlock restrict access', () => {
|
describe('XBlock restrict access', () => {
|
||||||
it('opens xblock restrict access modal successfully', async () => {
|
it('opens xblock restrict access modal successfully', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByTitle, getByTestId,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
|
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
|
||||||
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
|
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
|
||||||
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
|
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
const usageId = courseVerticalChildrenMock.children[0].block_id;
|
const usageId = courseVerticalChildrenMock.children[0].block_id;
|
||||||
expect(iframe).toBeInTheDocument();
|
expect(iframe).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -1813,7 +1876,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const configureModal = getByTestId('configure-modal');
|
const configureModal = screen.getByTestId('configure-modal');
|
||||||
|
|
||||||
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
|
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
|
||||||
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
|
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
|
||||||
@@ -1822,12 +1885,10 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes xblock restrict access modal when cancel button is clicked', async () => {
|
it('closes xblock restrict access modal when cancel button is clicked', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByTitle, queryByTestId, getByTestId,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toBeInTheDocument();
|
expect(iframe).toBeInTheDocument();
|
||||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||||
@@ -1835,7 +1896,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const configureModal = getByTestId('configure-modal');
|
const configureModal = screen.getByTestId('configure-modal');
|
||||||
expect(configureModal).toBeInTheDocument();
|
expect(configureModal).toBeInTheDocument();
|
||||||
userEvent.click(within(configureModal).getByRole('button', {
|
userEvent.click(within(configureModal).getByRole('button', {
|
||||||
name: configureModalMessages.cancelButton.defaultMessage,
|
name: configureModalMessages.cancelButton.defaultMessage,
|
||||||
@@ -1843,7 +1904,7 @@ describe('<CourseUnit />', () => {
|
|||||||
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
|
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles submit xblock restrict access data when save button is clicked', async () => {
|
it('handles submit xblock restrict access data when save button is clicked', async () => {
|
||||||
@@ -1854,15 +1915,13 @@ describe('<CourseUnit />', () => {
|
|||||||
})
|
})
|
||||||
.reply(200, { dummy: 'value' });
|
.reply(200, { dummy: 'value' });
|
||||||
|
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByTitle, getByRole, getByTestId, queryByTestId,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
|
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
|
||||||
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
|
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toBeInTheDocument();
|
expect(iframe).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1872,13 +1931,13 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const configureModal = await waitFor(() => getByTestId('configure-modal'));
|
const configureModal = await waitFor(() => screen.getByTestId('configure-modal'));
|
||||||
expect(configureModal).toBeInTheDocument();
|
expect(configureModal).toBeInTheDocument();
|
||||||
|
|
||||||
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
|
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
|
||||||
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
|
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
|
||||||
|
|
||||||
const restrictAccessSelect = getByRole('combobox', {
|
const restrictAccessSelect = screen.getByRole('combobox', {
|
||||||
name: configureModalMessages.restrictAccessTo.defaultMessage,
|
name: configureModalMessages.restrictAccessTo.defaultMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1908,17 +1967,17 @@ describe('<CourseUnit />', () => {
|
|||||||
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id));
|
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkLegacyEditModalOnEditMessage = async () => {
|
const checkLegacyEditModalOnEditMessage = async () => {
|
||||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const editButton = getByTestId('header-edit-button');
|
const editButton = screen.getByTestId('header-edit-button');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(xblocksIframe).toBeInTheDocument();
|
expect(xblocksIframe).toBeInTheDocument();
|
||||||
userEvent.click(editButton);
|
userEvent.click(editButton);
|
||||||
});
|
});
|
||||||
@@ -1953,7 +2012,6 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
describe('Library Content page', () => {
|
describe('Library Content page', () => {
|
||||||
const newUnitId = '12345';
|
const newUnitId = '12345';
|
||||||
const sequenceId = courseSectionVerticalMock.subsection_location;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
axiosMock
|
axiosMock
|
||||||
@@ -1970,20 +2028,6 @@ describe('<CourseUnit />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
axiosMock
|
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
|
||||||
.reply(200, {
|
|
||||||
...courseUnitIndexMock,
|
|
||||||
category: 'library_content',
|
|
||||||
ancestor_info: {
|
|
||||||
...courseUnitIndexMock.ancestor_info,
|
|
||||||
child_info: {
|
|
||||||
...courseUnitIndexMock.ancestor_info.child_info,
|
|
||||||
category: 'library_content',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to library content page on receive window event', async () => {
|
it('navigates to library content page on receive window event', async () => {
|
||||||
@@ -2003,8 +2047,8 @@ describe('<CourseUnit />', () => {
|
|||||||
findByTestId,
|
findByTestId,
|
||||||
} = render(<RootWrapper />);
|
} = render(<RootWrapper />);
|
||||||
|
|
||||||
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||||
|
|
||||||
const unitHeaderTitle = await findByTestId('unit-header-title');
|
const unitHeaderTitle = await findByTestId('unit-header-title');
|
||||||
await findByText(unitDisplayName);
|
await findByText(unitDisplayName);
|
||||||
@@ -2032,7 +2076,6 @@ describe('<CourseUnit />', () => {
|
|||||||
|
|
||||||
describe('Split Test Content page', () => {
|
describe('Split Test Content page', () => {
|
||||||
const newUnitId = '12345';
|
const newUnitId = '12345';
|
||||||
const sequenceId = courseSectionVerticalMock.subsection_location;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
axiosMock
|
axiosMock
|
||||||
@@ -2049,20 +2092,6 @@ describe('<CourseUnit />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
axiosMock
|
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
|
||||||
.reply(200, {
|
|
||||||
...courseUnitIndexMock,
|
|
||||||
category: 'split_test',
|
|
||||||
ancestor_info: {
|
|
||||||
...courseUnitIndexMock.ancestor_info,
|
|
||||||
child_info: {
|
|
||||||
...courseUnitIndexMock.ancestor_info.child_info,
|
|
||||||
category: 'split_test',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to split test content page on receive window event', async () => {
|
it('navigates to split test content page on receive window event', async () => {
|
||||||
@@ -2105,46 +2134,65 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render split test content page correctly', async () => {
|
it('should render split test content page correctly', async () => {
|
||||||
const {
|
render(<RootWrapper />);
|
||||||
getByText,
|
|
||||||
getByRole,
|
|
||||||
queryByRole,
|
|
||||||
getByTestId,
|
|
||||||
queryByText,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||||
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
|
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
|
||||||
|
|
||||||
waitFor(() => {
|
waitFor(() => {
|
||||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||||
|
|
||||||
expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
|
expect(screen.queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
|
||||||
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
|
||||||
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
|
||||||
|
|
||||||
expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
|
||||||
expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
|
||||||
|
|
||||||
// Sidebar
|
// Sidebar
|
||||||
const sidebarContent = [
|
const sidebarContent = [
|
||||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
|
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
|
||||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
|
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
|
||||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
|
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
|
||||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
|
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
|
||||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage.replaceAll('{bold_tag}', '') },
|
{
|
||||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage },
|
query: screen.queryByText,
|
||||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage },
|
name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage
|
||||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage },
|
.replaceAll('{bold_tag}', ''),
|
||||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage },
|
},
|
||||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage },
|
{
|
||||||
{ query: queryByRole, type: 'link', name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage },
|
query: screen.queryByRole,
|
||||||
|
type: 'heading',
|
||||||
|
name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: screen.queryByText,
|
||||||
|
name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: screen.queryByText,
|
||||||
|
name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: screen.queryByRole,
|
||||||
|
type: 'heading',
|
||||||
|
name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: screen.queryByText,
|
||||||
|
name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: screen.queryByRole,
|
||||||
|
type: 'link',
|
||||||
|
name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
sidebarContent.forEach(({ query, type, name }) => {
|
sidebarContent.forEach(({ query, type, name }) => {
|
||||||
@@ -2152,7 +2200,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
|
screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
|
||||||
).toHaveAttribute('href', helpLinkUrl);
|
).toHaveAttribute('href', helpLinkUrl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2165,7 +2213,7 @@ describe('<CourseUnit />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
|
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
|
||||||
const { getByTitle } = render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
|
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
|
||||||
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
|
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
|
||||||
|
|
||||||
@@ -2174,6 +2222,13 @@ describe('<CourseUnit />', () => {
|
|||||||
? { ...child, block_type: 'html' }
|
? { ...child, block_type: 'html' }
|
||||||
: child));
|
: child));
|
||||||
|
|
||||||
|
axiosMock
|
||||||
|
.onPost(postXBlockBaseApiUrl({
|
||||||
|
parent_locator: blockId,
|
||||||
|
duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id,
|
||||||
|
}))
|
||||||
|
.replyOnce(200, { locator: '1234567890' });
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||||
.reply(200, updatedCourseVerticalChildrenMock);
|
.reply(200, updatedCourseVerticalChildrenMock);
|
||||||
@@ -2181,7 +2236,7 @@ describe('<CourseUnit />', () => {
|
|||||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||||
expect(iframe).toBeInTheDocument();
|
expect(iframe).toBeInTheDocument();
|
||||||
simulatePostMessageEvent(messageTypes.currentXBlockId, {
|
simulatePostMessageEvent(messageTypes.currentXBlockId, {
|
||||||
id: targetBlockId,
|
id: targetBlockId,
|
||||||
@@ -2204,15 +2259,19 @@ describe('<CourseUnit />', () => {
|
|||||||
render(<RootWrapper />);
|
render(<RootWrapper />);
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
upstreamInfo: {
|
xblock_info: {
|
||||||
upstreamRef: 'lct:org:lib:unit:unit-1',
|
...courseSectionVerticalMock.xblock_info,
|
||||||
upstreamLink: 'some-link',
|
upstreamInfo: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
upstreamRef: 'lct:org:lib:unit:unit-1',
|
||||||
|
upstreamLink: 'some-link',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||||
|
|
||||||
expect(screen.getByText(/this unit can only be edited from the \./i)).toBeInTheDocument();
|
expect(screen.getByText(/this unit can only be edited from the \./i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
|||||||
@@ -1,1126 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
|
||||||
display_name: 'Getting Started',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'needs_attention',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: true,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
data: '',
|
|
||||||
metadata: {
|
|
||||||
display_name: 'Getting Started',
|
|
||||||
xml_attributes: {
|
|
||||||
filename: [
|
|
||||||
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
|
|
||||||
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ancestor_info: {
|
|
||||||
ancestors: [
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
|
||||||
display_name: 'Lesson 1 - Getting Started',
|
|
||||||
category: 'sequential',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'needs_attention',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: null,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
hide_after_due: false,
|
|
||||||
is_proctored_exam: false,
|
|
||||||
was_exam_ever_linked_with_external: false,
|
|
||||||
online_proctoring_rules: '',
|
|
||||||
is_practice_exam: false,
|
|
||||||
is_onboarding_exam: false,
|
|
||||||
is_time_limited: false,
|
|
||||||
exam_review_rules: '',
|
|
||||||
default_time_limit_minutes: null,
|
|
||||||
proctoring_exam_configuration_link: null,
|
|
||||||
supports_onboarding: false,
|
|
||||||
show_review_rules: true,
|
|
||||||
child_info: {
|
|
||||||
category: 'vertical',
|
|
||||||
display_name: 'Unit',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
|
||||||
display_name: 'Getting Started',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'needs_attention',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: true,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
|
||||||
display_name: 'Working with Videos',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
|
||||||
display_name: 'Videos on edX',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
|
||||||
display_name: 'Video Demonstrations',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
|
||||||
display_name: 'Video Presentation Styles',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
|
||||||
display_name: 'Interactive Questions',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
|
|
||||||
display_name: 'Exciting Labs and Tools',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
|
||||||
display_name: 'Reading Assignments',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
|
|
||||||
display_name: 'When Are Your Exams? ',
|
|
||||||
category: 'vertical',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: false,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
discussion_enabled: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions',
|
|
||||||
display_name: 'Example Week 1: Getting Started',
|
|
||||||
category: 'chapter',
|
|
||||||
has_children: true,
|
|
||||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
|
||||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: 'live',
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: null,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
highlights: [],
|
|
||||||
highlights_enabled: true,
|
|
||||||
highlights_preview_only: false,
|
|
||||||
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
|
||||||
display_name: 'Demonstration Course',
|
|
||||||
category: 'course',
|
|
||||||
has_children: true,
|
|
||||||
unit_level_discussions: false,
|
|
||||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
|
||||||
published: true,
|
|
||||||
published_on: 'Jan 03, 2024 at 08:57 UTC',
|
|
||||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course',
|
|
||||||
released_to_students: true,
|
|
||||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
|
||||||
visibility_state: null,
|
|
||||||
has_explicit_staff_lock: false,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
graded: false,
|
|
||||||
due_date: '',
|
|
||||||
due: null,
|
|
||||||
relative_weeks_due: null,
|
|
||||||
format: null,
|
|
||||||
course_graders: [
|
|
||||||
'Homework',
|
|
||||||
'Exam',
|
|
||||||
],
|
|
||||||
has_changes: null,
|
|
||||||
actions: {
|
|
||||||
deletable: true,
|
|
||||||
draggable: true,
|
|
||||||
childAddable: true,
|
|
||||||
duplicable: true,
|
|
||||||
},
|
|
||||||
explanatory_message: null,
|
|
||||||
group_access: {},
|
|
||||||
user_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
show_correctness: 'always',
|
|
||||||
highlights_enabled_for_messaging: false,
|
|
||||||
highlights_enabled: true,
|
|
||||||
highlights_preview_only: false,
|
|
||||||
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
|
||||||
enable_proctored_exams: false,
|
|
||||||
create_zendesk_tickets: true,
|
|
||||||
enable_timed_exams: true,
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
ancestor_has_staff_lock: false,
|
|
||||||
user_partition_info: {
|
|
||||||
selectable_partitions: [
|
|
||||||
{
|
|
||||||
id: 50,
|
|
||||||
name: 'Enrollment Track Groups',
|
|
||||||
scheme: 'enrollment_track',
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Verified Certificate',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Audit',
|
|
||||||
selected: false,
|
|
||||||
deleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selected_partition_index: -1,
|
|
||||||
selected_groups_label: '',
|
|
||||||
},
|
|
||||||
enable_copy_paste_units: false,
|
|
||||||
edited_by: 'edx',
|
|
||||||
published_by: null,
|
|
||||||
currently_visible_to_students: true,
|
|
||||||
has_partition_group_components: false,
|
|
||||||
release_date_from: 'Section "Example Week 1: Getting Started"',
|
|
||||||
staff_lock_from: null,
|
|
||||||
upstreamInfo: {
|
|
||||||
upstreamLink: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export { default as courseUnitIndexMock } from './courseUnitIndex';
|
|
||||||
export { default as courseSectionVerticalMock } from './courseSectionVertical';
|
export { default as courseSectionVerticalMock } from './courseSectionVertical';
|
||||||
export { default as courseUnitMock } from './courseUnit';
|
export { default as courseUnitMock } from './courseUnit';
|
||||||
export { default as courseCreateXblockMock } from './courseCreateXblock';
|
export { default as courseCreateXblockMock } from './courseCreateXblock';
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
ActionRow, Button, StandardModal, useToggle,
|
ActionRow, Button, StandardModal, useToggle,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
|
|
||||||
import { getCourseSectionVertical } from '../data/selectors';
|
import { getCourseSectionVertical } from '../data/selectors';
|
||||||
|
import { getWaffleFlags } from '../../data/selectors';
|
||||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||||
import ComponentModalView from './add-component-modals/ComponentModalView';
|
import ComponentModalView from './add-component-modals/ComponentModalView';
|
||||||
import AddComponentButton from './add-component-btn';
|
import AddComponentButton from './add-component-btn';
|
||||||
@@ -16,6 +17,8 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
|
|||||||
import { messageTypes } from '../constants';
|
import { messageTypes } from '../constants';
|
||||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||||
import { useEventListener } from '../../generic/hooks';
|
import { useEventListener } from '../../generic/hooks';
|
||||||
|
import VideoSelectorPage from '../../editors/VideoSelectorPage';
|
||||||
|
import EditorPage from '../../editors/EditorPage';
|
||||||
|
|
||||||
const AddComponent = ({
|
const AddComponent = ({
|
||||||
parentLocator,
|
parentLocator,
|
||||||
@@ -24,7 +27,6 @@ const AddComponent = ({
|
|||||||
addComponentTemplateData,
|
addComponentTemplateData,
|
||||||
handleCreateNewCourseXBlock,
|
handleCreateNewCourseXBlock,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
|
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
|
||||||
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
|
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
|
||||||
@@ -32,10 +34,17 @@ const AddComponent = ({
|
|||||||
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
|
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
|
||||||
const blockId = addComponentTemplateData.parentLocator || parentLocator;
|
const blockId = addComponentTemplateData.parentLocator || parentLocator;
|
||||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||||
|
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
|
||||||
|
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
|
||||||
|
|
||||||
|
const [blockType, setBlockType] = useState(null);
|
||||||
|
const [courseId, setCourseId] = useState(null);
|
||||||
|
const [newBlockId, setNewBlockId] = useState(null);
|
||||||
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
|
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
|
||||||
const [selectedComponents, setSelectedComponents] = useState([]);
|
const [selectedComponents, setSelectedComponents] = useState([]);
|
||||||
const [usageId, setUsageId] = useState(null);
|
const [usageId, setUsageId] = useState(null);
|
||||||
const { sendMessageToIframe } = useIframe();
|
const { sendMessageToIframe } = useIframe();
|
||||||
|
const { useVideoGalleryFlow, useReactMarkdownEditor } = useSelector(getWaffleFlags);
|
||||||
|
|
||||||
const receiveMessage = useCallback(({ data: { type, payload } }) => {
|
const receiveMessage = useCallback(({ data: { type, payload } }) => {
|
||||||
if (type === messageTypes.showMultipleComponentPicker) {
|
if (type === messageTypes.showMultipleComponentPicker) {
|
||||||
@@ -54,6 +63,12 @@ const AddComponent = ({
|
|||||||
closeSelectLibraryContentModal();
|
closeSelectLibraryContentModal();
|
||||||
}, [selectedComponents]);
|
}, [selectedComponents]);
|
||||||
|
|
||||||
|
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
|
||||||
|
closeXBlockEditorModal();
|
||||||
|
closeVideoSelectorModal();
|
||||||
|
sendMessageToIframe(messageTypes.refreshXBlock, null);
|
||||||
|
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
|
||||||
|
|
||||||
const handleLibraryV2Selection = useCallback((selection) => {
|
const handleLibraryV2Selection = useCallback((selection) => {
|
||||||
handleCreateNewCourseXBlock({
|
handleCreateNewCourseXBlock({
|
||||||
type: COMPONENT_TYPES.libraryV2,
|
type: COMPONENT_TYPES.libraryV2,
|
||||||
@@ -71,12 +86,28 @@ const AddComponent = ({
|
|||||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
|
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
|
||||||
break;
|
break;
|
||||||
case COMPONENT_TYPES.problem:
|
case COMPONENT_TYPES.problem:
|
||||||
case COMPONENT_TYPES.video:
|
|
||||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
||||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
setCourseId(courseKey);
|
||||||
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
|
setBlockType(type);
|
||||||
|
setNewBlockId(locator);
|
||||||
|
showXBlockEditorModal();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case COMPONENT_TYPES.video:
|
||||||
|
handleCreateNewCourseXBlock(
|
||||||
|
{ type, parentLocator: blockId },
|
||||||
|
/* istanbul ignore next */ ({ courseKey, locator }) => {
|
||||||
|
setCourseId(courseKey);
|
||||||
|
setBlockType(type);
|
||||||
|
setNewBlockId(locator);
|
||||||
|
if (useVideoGalleryFlow) {
|
||||||
|
showVideoSelectorModal();
|
||||||
|
} else {
|
||||||
|
showXBlockEditorModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
// TODO: The library functional will be a bit different of current legacy (CMS)
|
// TODO: The library functional will be a bit different of current legacy (CMS)
|
||||||
// behaviour and this ticket is on hold (blocked by other development team).
|
// behaviour and this ticket is on hold (blocked by other development team).
|
||||||
case COMPONENT_TYPES.library:
|
case COMPONENT_TYPES.library:
|
||||||
@@ -99,9 +130,11 @@ const AddComponent = ({
|
|||||||
type,
|
type,
|
||||||
boilerplate: moduleName,
|
boilerplate: moduleName,
|
||||||
parentLocator: blockId,
|
parentLocator: blockId,
|
||||||
}, ({ courseKey, locator }) => {
|
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
|
||||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
setCourseId(courseKey);
|
||||||
navigate(`/course/${courseKey}/editor/html/${locator}`);
|
setBlockType(type);
|
||||||
|
setNewBlockId(locator);
|
||||||
|
showXBlockEditorModal();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -201,6 +234,38 @@ const AddComponent = ({
|
|||||||
onChangeComponentSelection={setSelectedComponents}
|
onChangeComponentSelection={setSelectedComponents}
|
||||||
/>
|
/>
|
||||||
</StandardModal>
|
</StandardModal>
|
||||||
|
<StandardModal
|
||||||
|
title={intl.formatMessage(messages.videoPickerModalTitle)}
|
||||||
|
isOpen={isVideoSelectorModalOpen}
|
||||||
|
onClose={closeVideoSelectorModal}
|
||||||
|
isOverflowVisible={false}
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
<div className="selector-page">
|
||||||
|
<VideoSelectorPage
|
||||||
|
blockId={newBlockId}
|
||||||
|
courseId={courseId}
|
||||||
|
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
|
||||||
|
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||||
|
onCancel={closeVideoSelectorModal}
|
||||||
|
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StandardModal>
|
||||||
|
{isXBlockEditorModalOpen && (
|
||||||
|
<div className="editor-page">
|
||||||
|
<EditorPage
|
||||||
|
courseId={courseId}
|
||||||
|
blockType={blockType}
|
||||||
|
blockId={newBlockId}
|
||||||
|
isMarkdownEditorEnabledForCourse={useReactMarkdownEditor}
|
||||||
|
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
|
||||||
|
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||||
|
onClose={closeXBlockEditorModal}
|
||||||
|
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Add selected components',
|
defaultMessage: 'Add selected components',
|
||||||
description: 'Problem bank component add button text.',
|
description: 'Problem bank component add button text.',
|
||||||
},
|
},
|
||||||
|
videoPickerModalTitle: {
|
||||||
|
id: 'course-authoring.course-unit.modal.video-title.text',
|
||||||
|
defaultMessage: 'Select video',
|
||||||
|
description: 'Video picker modal title.',
|
||||||
|
},
|
||||||
modalContainerTitle: {
|
modalContainerTitle: {
|
||||||
id: 'course-authoring.course-unit.modal.container.title',
|
id: 'course-authoring.course-unit.modal.container.title',
|
||||||
defaultMessage: 'Add {componentTitle} component',
|
defaultMessage: 'Add {componentTitle} component',
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
} from '../../testUtils';
|
} from '../../testUtils';
|
||||||
|
|
||||||
import { executeThunk } from '../../utils';
|
import { executeThunk } from '../../utils';
|
||||||
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
|
import { getCourseSectionVerticalApiUrl } from '../data/api';
|
||||||
import { getApiWaffleFlagsUrl } from '../../data/api';
|
import { getApiWaffleFlagsUrl } from '../../data/api';
|
||||||
import { fetchWaffleFlags } from '../../data/thunks';
|
import { fetchWaffleFlags } from '../../data/thunks';
|
||||||
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
|
import { fetchCourseSectionVerticalData } from '../data/thunk';
|
||||||
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
|
import { courseSectionVerticalMock } from '../__mocks__';
|
||||||
import Breadcrumbs from './Breadcrumbs';
|
import Breadcrumbs from './Breadcrumbs';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
@@ -43,9 +43,9 @@ describe('<Breadcrumbs />', () => {
|
|||||||
reduxStore = mocks.reduxStore;
|
reduxStore = mocks.reduxStore;
|
||||||
|
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), reduxStore.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch);
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||||
.reply(200, courseSectionVerticalMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
|
|||||||
@@ -12,16 +12,19 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
|
|||||||
const isLastUnit = !nextUrl;
|
const isLastUnit = !nextUrl;
|
||||||
const sequenceIds = useSelector(getSequenceIds);
|
const sequenceIds = useSelector(getSequenceIds);
|
||||||
const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
|
const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
|
||||||
const unitIndex = sequence.unitIds.indexOf(currentUnitId);
|
let unitIndex = sequence?.unitIds.indexOf(currentUnitId);
|
||||||
|
|
||||||
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
|
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
|
||||||
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
|
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
|
||||||
|
if (!unitIndex) {
|
||||||
|
// Handle case where unitIndex is not found
|
||||||
|
unitIndex = 0;
|
||||||
|
}
|
||||||
let nextLink;
|
let nextLink;
|
||||||
const nextIndex = unitIndex + 1;
|
const nextIndex = unitIndex + 1;
|
||||||
|
|
||||||
if (nextIndex < sequence.unitIds.length) {
|
if (nextIndex < sequence?.unitIds.length) {
|
||||||
const nextUnitId = sequence.unitIds[nextIndex];
|
const nextUnitId = sequence?.unitIds[nextIndex];
|
||||||
nextLink = `/course/${courseId}/container/${nextUnitId}/${currentSequenceId}`;
|
nextLink = `/course/${courseId}/container/${nextUnitId}/${currentSequenceId}`;
|
||||||
} else if (nextSequenceId) {
|
} else if (nextSequenceId) {
|
||||||
const pathToNextUnit = decodeURIComponent(nextUrl);
|
const pathToNextUnit = decodeURIComponent(nextUrl);
|
||||||
@@ -32,7 +35,7 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
|
|||||||
const previousIndex = unitIndex - 1;
|
const previousIndex = unitIndex - 1;
|
||||||
|
|
||||||
if (previousIndex >= 0) {
|
if (previousIndex >= 0) {
|
||||||
const previousUnitId = sequence.unitIds[previousIndex];
|
const previousUnitId = sequence?.unitIds[previousIndex];
|
||||||
previousLink = `/course/${courseId}/container/${previousUnitId}/${currentSequenceId}`;
|
previousLink = `/course/${courseId}/container/${previousUnitId}/${currentSequenceId}`;
|
||||||
} else if (previousSequenceId) {
|
} else if (previousSequenceId) {
|
||||||
const pathToPreviousUnit = decodeURIComponent(prevUrl);
|
const pathToPreviousUnit = decodeURIComponent(prevUrl);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const SequenceNavigation = ({
|
|||||||
|
|
||||||
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
|
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
|
||||||
const renderUnitButtons = () => {
|
const renderUnitButtons = () => {
|
||||||
if (sequence.unitIds?.length === 0 || unitId === null) {
|
if (sequence.unitIds.length === 0 || unitId === null) {
|
||||||
return (
|
return (
|
||||||
<div style={{ flexBasis: '100%', minWidth: 0, borderBottom: 'solid 1px #EAEAEA' }} />
|
<div style={{ flexBasis: '100%', minWidth: 0, borderBottom: 'solid 1px #EAEAEA' }} />
|
||||||
);
|
);
|
||||||
@@ -43,7 +43,7 @@ const SequenceNavigation = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SequenceNavigationTabs
|
<SequenceNavigationTabs
|
||||||
unitIds={sequence.unitIds || []}
|
unitIds={sequence?.unitIds || []}
|
||||||
unitId={unitId}
|
unitId={unitId}
|
||||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||||
showPasteUnit={showPasteUnit}
|
showPasteUnit={showPasteUnit}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
|||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
import { PUBLISH_TYPES } from '../constants';
|
import { PUBLISH_TYPES } from '../constants';
|
||||||
import { normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
|
import { isUnitReadOnly, normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
|
||||||
|
|
||||||
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||||
|
|
||||||
export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
|
|
||||||
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
|
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
|
||||||
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
|
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
|
||||||
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
|
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
|
||||||
@@ -15,18 +14,6 @@ export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/cour
|
|||||||
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
|
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
|
||||||
export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
|
export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get course unit.
|
|
||||||
* @param {string} unitId
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
|
||||||
export async function getCourseUnitData(unitId) {
|
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
|
||||||
.get(getCourseUnitApiUrl(unitId));
|
|
||||||
|
|
||||||
return camelCaseObject(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit course unit display name.
|
* Edit course unit display name.
|
||||||
* @param {string} unitId
|
* @param {string} unitId
|
||||||
@@ -45,15 +32,18 @@ export async function editUnitDisplayName(unitId, displayName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an object containing course section vertical data.
|
* Fetch vertical block data from the container_handler endpoint.
|
||||||
* @param {string} unitId
|
* @param {string} unitId
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
export async function getCourseSectionVerticalData(unitId) {
|
export async function getVerticalData(unitId) {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.get(getCourseSectionVerticalApiUrl(unitId));
|
.get(getCourseSectionVerticalApiUrl(unitId));
|
||||||
|
|
||||||
return normalizeCourseSectionVerticalData(data);
|
const courseSectionVerticalData = normalizeCourseSectionVerticalData(data);
|
||||||
|
courseSectionVerticalData.xblockInfo.readOnly = isUnitReadOnly(courseSectionVerticalData.xblockInfo);
|
||||||
|
|
||||||
|
return courseSectionVerticalData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RequestStatus } from 'CourseAuthoring/data/constants';
|
import { RequestStatus } from 'CourseAuthoring/data/constants';
|
||||||
|
|
||||||
export const getCourseUnitData = (state) => state.courseUnit.unit;
|
export const getCourseUnitData = (state) => state.courseUnit.courseSectionVertical.xblockInfo ?? {};
|
||||||
export const getCanEdit = (state) => state.courseUnit.canEdit;
|
export const getCanEdit = (state) => state.courseUnit.canEdit;
|
||||||
export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices;
|
export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices;
|
||||||
export const getCourseUnit = (state) => state.courseUnit;
|
export const getCourseUnit = (state) => state.courseUnit;
|
||||||
@@ -16,7 +16,7 @@ export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerti
|
|||||||
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo;
|
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo;
|
||||||
export const getCourseOutlineInfoLoadingStatus = (state) => state.courseUnit.courseOutlineInfoLoadingStatus;
|
export const getCourseOutlineInfoLoadingStatus = (state) => state.courseUnit.courseOutlineInfoLoadingStatus;
|
||||||
export const getMovedXBlockParams = (state) => state.courseUnit.movedXBlockParams;
|
export const getMovedXBlockParams = (state) => state.courseUnit.movedXBlockParams;
|
||||||
const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
|
export const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
|
||||||
export const getIsLoading = createSelector(
|
export const getIsLoading = createSelector(
|
||||||
[getLoadingStatuses],
|
[getLoadingStatuses],
|
||||||
loadingStatus => Object.values(loadingStatus)
|
loadingStatus => Object.values(loadingStatus)
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ const slice = createSlice({
|
|||||||
isTitleEditFormOpen: false,
|
isTitleEditFormOpen: false,
|
||||||
canEdit: true,
|
canEdit: true,
|
||||||
loadingStatus: {
|
loadingStatus: {
|
||||||
fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS,
|
|
||||||
courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS,
|
courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||||
courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS,
|
courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||||
},
|
},
|
||||||
unit: {},
|
|
||||||
courseSectionVertical: {},
|
courseSectionVertical: {},
|
||||||
courseVerticalChildren: { children: [], isPublished: true },
|
courseVerticalChildren: { children: [], isPublished: true },
|
||||||
staticFileNotices: {},
|
staticFileNotices: {},
|
||||||
@@ -31,15 +29,6 @@ const slice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
fetchCourseItemSuccess: (state, { payload }) => {
|
|
||||||
state.unit = payload;
|
|
||||||
},
|
|
||||||
updateLoadingCourseUnitStatus: (state, { payload }) => {
|
|
||||||
state.loadingStatus = {
|
|
||||||
...state.loadingStatus,
|
|
||||||
fetchUnitLoadingStatus: payload.status,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
updateQueryPendingStatus: (state, { payload }) => {
|
updateQueryPendingStatus: (state, { payload }) => {
|
||||||
state.isQueryPending = payload;
|
state.isQueryPending = payload;
|
||||||
},
|
},
|
||||||
@@ -81,12 +70,6 @@ const slice = createSlice({
|
|||||||
createUnitXblockLoadingStatus: payload.status,
|
createUnitXblockLoadingStatus: payload.status,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addNewUnitStatus: (state, { payload }) => {
|
|
||||||
state.loadingStatus = {
|
|
||||||
...state.loadingStatus,
|
|
||||||
fetchUnitLoadingStatus: payload.status,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
updateCourseVerticalChildren: (state, { payload }) => {
|
updateCourseVerticalChildren: (state, { payload }) => {
|
||||||
state.courseVerticalChildren = payload;
|
state.courseVerticalChildren = payload;
|
||||||
},
|
},
|
||||||
@@ -109,8 +92,6 @@ const slice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
fetchCourseItemSuccess,
|
|
||||||
updateLoadingCourseUnitStatus,
|
|
||||||
updateSavingStatus,
|
updateSavingStatus,
|
||||||
updateModel,
|
updateModel,
|
||||||
fetchSequenceRequest,
|
fetchSequenceRequest,
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ import { NOTIFICATION_MESSAGES } from '../../constants';
|
|||||||
import { updateModel, updateModels } from '../../generic/model-store';
|
import { updateModel, updateModels } from '../../generic/model-store';
|
||||||
import { messageTypes } from '../constants';
|
import { messageTypes } from '../constants';
|
||||||
import {
|
import {
|
||||||
getCourseUnitData,
|
|
||||||
editUnitDisplayName,
|
editUnitDisplayName,
|
||||||
getCourseSectionVerticalData,
|
getVerticalData,
|
||||||
createCourseXblock,
|
createCourseXblock,
|
||||||
getCourseVerticalChildren,
|
getCourseVerticalChildren,
|
||||||
handleCourseUnitVisibilityAndData,
|
handleCourseUnitVisibilityAndData,
|
||||||
@@ -22,8 +21,6 @@ import {
|
|||||||
patchUnitItem,
|
patchUnitItem,
|
||||||
} from './api';
|
} from './api';
|
||||||
import {
|
import {
|
||||||
updateLoadingCourseUnitStatus,
|
|
||||||
fetchCourseItemSuccess,
|
|
||||||
updateSavingStatus,
|
updateSavingStatus,
|
||||||
fetchSequenceRequest,
|
fetchSequenceRequest,
|
||||||
fetchSequenceFailure,
|
fetchSequenceFailure,
|
||||||
@@ -38,25 +35,7 @@ import {
|
|||||||
updateCourseOutlineInfoLoadingStatus,
|
updateCourseOutlineInfoLoadingStatus,
|
||||||
updateMovedXBlockParams,
|
updateMovedXBlockParams,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
import { getNotificationMessage, isUnitReadOnly } from './utils';
|
import { getNotificationMessage } from './utils';
|
||||||
|
|
||||||
export function fetchCourseUnitQuery(courseId) {
|
|
||||||
return async (dispatch) => {
|
|
||||||
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.IN_PROGRESS }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const courseUnit = await getCourseUnitData(courseId);
|
|
||||||
courseUnit.readOnly = isUnitReadOnly(courseUnit);
|
|
||||||
|
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
|
||||||
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL }));
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.FAILED }));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchCourseSectionVerticalData(courseId, sequenceId) {
|
export function fetchCourseSectionVerticalData(courseId, sequenceId) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
@@ -64,7 +43,7 @@ export function fetchCourseSectionVerticalData(courseId, sequenceId) {
|
|||||||
dispatch(fetchSequenceRequest({ sequenceId }));
|
dispatch(fetchSequenceRequest({ sequenceId }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const courseSectionVerticalData = await getCourseSectionVerticalData(courseId);
|
const courseSectionVerticalData = await getVerticalData(courseId);
|
||||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
@@ -95,8 +74,7 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
|
|||||||
try {
|
try {
|
||||||
await editUnitDisplayName(itemId, displayName).then(async (result) => {
|
await editUnitDisplayName(itemId, displayName).then(async (result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
const courseUnit = await getCourseUnitData(itemId);
|
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||||
const courseSectionVerticalData = await getCourseSectionVerticalData(itemId);
|
|
||||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
@@ -108,7 +86,6 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
|
|||||||
models: courseSectionVerticalData.units || [],
|
models: courseSectionVerticalData.units || [],
|
||||||
}));
|
}));
|
||||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
|
||||||
dispatch(hideProcessingNotification());
|
dispatch(hideProcessingNotification());
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
}
|
}
|
||||||
@@ -147,8 +124,8 @@ export function editCourseUnitVisibilityAndData(
|
|||||||
if (callback) {
|
if (callback) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
const courseUnit = await getCourseUnitData(blockId);
|
const courseSectionVerticalData = await getVerticalData(blockId);
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
|
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
|
||||||
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
|
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
|
||||||
dispatch(hideProcessingNotification());
|
dispatch(hideProcessingNotification());
|
||||||
@@ -175,7 +152,7 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
|
|||||||
if (result) {
|
if (result) {
|
||||||
const formattedResult = camelCaseObject(result);
|
const formattedResult = camelCaseObject(result);
|
||||||
if (body.category === 'vertical') {
|
if (body.category === 'vertical') {
|
||||||
const courseSectionVerticalData = await getCourseSectionVerticalData(formattedResult.locator);
|
const courseSectionVerticalData = await getVerticalData(formattedResult.locator);
|
||||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
}
|
}
|
||||||
if (body.stagedContent) {
|
if (body.stagedContent) {
|
||||||
@@ -195,8 +172,8 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
|
|||||||
sendMessageToIframe(messageTypes.addXBlock, { data: result });
|
sendMessageToIframe(messageTypes.addXBlock, { data: result });
|
||||||
}
|
}
|
||||||
const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId;
|
const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId;
|
||||||
const courseUnit = await getCourseUnitData(currentBlockId);
|
const courseSectionVerticalData = await getVerticalData(currentBlockId);
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -241,8 +218,8 @@ export function deleteUnitItemQuery(itemId, xblockId, sendMessageToIframe) {
|
|||||||
try {
|
try {
|
||||||
await deleteUnitItem(xblockId);
|
await deleteUnitItem(xblockId);
|
||||||
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
|
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
|
||||||
const courseUnit = await getCourseUnitData(itemId);
|
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
dispatch(hideProcessingNotification());
|
dispatch(hideProcessingNotification());
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -260,8 +237,10 @@ export function duplicateUnitItemQuery(itemId, xblockId, callback) {
|
|||||||
try {
|
try {
|
||||||
const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId);
|
const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId);
|
||||||
callback(courseKey, locator);
|
callback(courseKey, locator);
|
||||||
const courseUnit = await getCourseUnitData(itemId);
|
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
|
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
|
||||||
|
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
|
||||||
dispatch(hideProcessingNotification());
|
dispatch(hideProcessingNotification());
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -315,8 +294,8 @@ export function patchUnitItemQuery({
|
|||||||
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||||
callbackFn(sourceLocator);
|
callbackFn(sourceLocator);
|
||||||
try {
|
try {
|
||||||
const courseUnit = await getCourseUnitData(currentParentLocator);
|
const courseSectionVerticalData = await getVerticalData(currentParentLocator);
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleResponseErrors(error, dispatch, updateSavingStatus);
|
handleResponseErrors(error, dispatch, updateSavingStatus);
|
||||||
}
|
}
|
||||||
@@ -334,8 +313,8 @@ export function updateCourseUnitSidebar(itemId) {
|
|||||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const courseUnit = await getCourseUnitData(itemId);
|
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||||
dispatch(hideProcessingNotification());
|
dispatch(hideProcessingNotification());
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
|||||||
|
|
||||||
import initializeStore from '../../store';
|
import initializeStore from '../../store';
|
||||||
import { executeThunk } from '../../utils';
|
import { executeThunk } from '../../utils';
|
||||||
import { getCourseUnitApiUrl } from '../data/api';
|
import { getCourseSectionVerticalApiUrl } from '../data/api';
|
||||||
import { fetchCourseUnitQuery } from '../data/thunk';
|
import { fetchCourseSectionVerticalData } from '../data/thunk';
|
||||||
import { courseUnitIndexMock } from '../__mocks__';
|
import { courseSectionVerticalMock } from '../__mocks__';
|
||||||
import HeaderTitle from './HeaderTitle';
|
import HeaderTitle from './HeaderTitle';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@ describe('<HeaderTitle />', () => {
|
|||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, courseUnitIndexMock);
|
.reply(200, courseSectionVerticalMock);
|
||||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render HeaderTitle component correctly', () => {
|
it('render HeaderTitle component correctly', () => {
|
||||||
@@ -80,14 +80,18 @@ describe('<HeaderTitle />', () => {
|
|||||||
// Override mock unit with one sourced from an upstream library
|
// Override mock unit with one sourced from an upstream library
|
||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
upstreamInfo: {
|
xblock_info: {
|
||||||
upstreamRef: 'lct:org:lib:unit:unit-1',
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
upstreamInfo: {
|
||||||
|
...courseSectionVerticalMock.xblock_info.upstreamInfo,
|
||||||
|
upstreamRef: 'lct:org:lib:unit:unit-1',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
|
|
||||||
const { getByRole } = renderComponent();
|
const { getByRole } = renderComponent();
|
||||||
|
|
||||||
@@ -122,16 +126,19 @@ describe('<HeaderTitle />', () => {
|
|||||||
|
|
||||||
it('displays a visibility message with the selected groups for the unit', async () => {
|
it('displays a visibility message with the selected groups for the unit', async () => {
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
user_partition_info: {
|
xblock_info: {
|
||||||
...courseUnitIndexMock.user_partition_info,
|
...courseSectionVerticalMock.xblock_info,
|
||||||
selected_partition_index: 1,
|
user_partition_info: {
|
||||||
selected_groups_label: 'Visibility group 1',
|
...courseSectionVerticalMock.xblock_info.user_partition_info,
|
||||||
|
selected_partition_index: 1,
|
||||||
|
selected_groups_label: 'Visibility group 1',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
const { getByText } = renderComponent();
|
const { getByText } = renderComponent();
|
||||||
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
|
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
|
||||||
.replace('{selectedGroupsLabel}', 'Visibility group 1');
|
.replace('{selectedGroupsLabel}', 'Visibility group 1');
|
||||||
@@ -143,12 +150,15 @@ describe('<HeaderTitle />', () => {
|
|||||||
|
|
||||||
it('displays a visibility message with the selected groups for some of xblock', async () => {
|
it('displays a visibility message with the selected groups for some of xblock', async () => {
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(blockId))
|
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
...courseUnitIndexMock,
|
...courseSectionVerticalMock,
|
||||||
has_partition_group_components: true,
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
has_partition_group_components: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||||
const { getByText } = renderComponent();
|
const { getByText } = renderComponent();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import {
|
|||||||
editCourseItemQuery,
|
editCourseItemQuery,
|
||||||
editCourseUnitVisibilityAndData,
|
editCourseUnitVisibilityAndData,
|
||||||
fetchCourseSectionVerticalData,
|
fetchCourseSectionVerticalData,
|
||||||
fetchCourseUnitQuery,
|
|
||||||
fetchCourseVerticalChildrenData,
|
fetchCourseVerticalChildrenData,
|
||||||
getCourseOutlineInfoQuery,
|
getCourseOutlineInfoQuery,
|
||||||
patchUnitItemQuery,
|
patchUnitItemQuery,
|
||||||
|
updateCourseUnitSidebar,
|
||||||
} from './data/thunk';
|
} from './data/thunk';
|
||||||
import {
|
import {
|
||||||
getCanEdit,
|
getCanEdit,
|
||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
getSavingStatus,
|
getSavingStatus,
|
||||||
getSequenceStatus,
|
getSequenceStatus,
|
||||||
getStaticFileNotices,
|
getStaticFileNotices,
|
||||||
|
getLoadingStatuses,
|
||||||
} from './data/selectors';
|
} from './data/selectors';
|
||||||
import {
|
import {
|
||||||
changeEditTitleFormOpen,
|
changeEditTitleFormOpen,
|
||||||
@@ -51,6 +52,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
|||||||
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
|
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
|
||||||
|
|
||||||
const courseUnit = useSelector(getCourseUnitData);
|
const courseUnit = useSelector(getCourseUnitData);
|
||||||
|
const courseUnitLoadingStatus = useSelector(getLoadingStatuses);
|
||||||
const savingStatus = useSelector(getSavingStatus);
|
const savingStatus = useSelector(getSavingStatus);
|
||||||
const isLoading = useSelector(getIsLoading);
|
const isLoading = useSelector(getIsLoading);
|
||||||
const errorMessage = useSelector(getErrorMessage);
|
const errorMessage = useSelector(getErrorMessage);
|
||||||
@@ -196,7 +198,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
|||||||
}, [savingStatus]);
|
}, [savingStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchCourseUnitQuery(blockId));
|
|
||||||
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
|
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
|
||||||
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
|
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
|
||||||
handleNavigate(sequenceId);
|
handleNavigate(sequenceId);
|
||||||
@@ -215,9 +216,27 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
|||||||
}
|
}
|
||||||
}, [isMoveModalOpen]);
|
}, [isMoveModalOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePageRefreshUsingStorage = (event) => {
|
||||||
|
// ignoring tests for if block, because it triggers when someone
|
||||||
|
// edits the component using editor which has a separate store
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (event.key === 'courseRefreshTriggerOnComponentEditSave') {
|
||||||
|
dispatch(updateCourseUnitSidebar(blockId));
|
||||||
|
localStorage.removeItem(event.key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('storage', handlePageRefreshUsingStorage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', handlePageRefreshUsingStorage);
|
||||||
|
};
|
||||||
|
}, [blockId, sequenceId, isSplitTestType]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sequenceId,
|
sequenceId,
|
||||||
courseUnit,
|
courseUnit,
|
||||||
|
courseUnitLoadingStatus,
|
||||||
unitTitle,
|
unitTitle,
|
||||||
unitCategory,
|
unitCategory,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.'
|
|||||||
import { messageTypes } from '../constants';
|
import { messageTypes } from '../constants';
|
||||||
import { libraryBlockChangesUrl } from '../data/api';
|
import { libraryBlockChangesUrl } from '../data/api';
|
||||||
import { ToastActionData } from '../../generic/toast-context';
|
import { ToastActionData } from '../../generic/toast-context';
|
||||||
import { getLibraryBlockMetadataUrl, getLibraryContainerApiUrl } from '../../library-authoring/data/api';
|
|
||||||
|
|
||||||
const usageKey = 'some-id';
|
const usageKey = 'some-id';
|
||||||
const defaultEventData: LibraryChangesMessageData = {
|
const defaultEventData: LibraryChangesMessageData = {
|
||||||
@@ -66,7 +65,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
|||||||
expect(await screen.findByRole('tab', { name: 'Old version' })).toBeInTheDocument();
|
expect(await screen.findByRole('tab', { name: 'Old version' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders displayName for units', async () => {
|
it('renders default displayName for units with no displayName', async () => {
|
||||||
render({ ...defaultEventData, isVertical: true, displayName: '' });
|
render({ ...defaultEventData, isVertical: true, displayName: '' });
|
||||||
|
|
||||||
expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument();
|
expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument();
|
||||||
@@ -78,24 +77,6 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
|||||||
expect(await screen.findByText('Preview changes: Component')).toBeInTheDocument();
|
expect(await screen.findByText('Preview changes: Component')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders both new and old title if they are different', async () => {
|
|
||||||
axiosMock.onGet(getLibraryBlockMetadataUrl(defaultEventData.upstreamBlockId)).reply(200, {
|
|
||||||
displayName: 'New test block',
|
|
||||||
});
|
|
||||||
render();
|
|
||||||
|
|
||||||
expect(await screen.findByText('Preview changes: Test block -> New test block')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders both new and old title if they are different on units', async () => {
|
|
||||||
axiosMock.onGet(getLibraryContainerApiUrl(defaultEventData.upstreamBlockId)).reply(200, {
|
|
||||||
displayName: 'New test Unit',
|
|
||||||
});
|
|
||||||
render({ ...defaultEventData, isVertical: true, displayName: 'Test Unit' });
|
|
||||||
|
|
||||||
expect(await screen.findByText('Preview changes: Test Unit -> New test Unit')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accept changes works', async () => {
|
it('accept changes works', async () => {
|
||||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||||
render();
|
render();
|
||||||
@@ -104,7 +85,10 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
|||||||
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
|
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
|
||||||
userEvent.click(acceptBtn);
|
userEvent.click(acceptBtn);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
|
expect(mockSendMessageToIframe).toHaveBeenCalledWith(
|
||||||
|
messageTypes.completeXBlockEditing,
|
||||||
|
{ locator: usageKey },
|
||||||
|
);
|
||||||
expect(axiosMock.history.post.length).toEqual(1);
|
expect(axiosMock.history.post.length).toEqual(1);
|
||||||
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
||||||
});
|
});
|
||||||
@@ -119,7 +103,6 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
|||||||
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
|
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
|
||||||
userEvent.click(acceptBtn);
|
userEvent.click(acceptBtn);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockSendMessageToIframe).not.toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
|
|
||||||
expect(axiosMock.history.post.length).toEqual(1);
|
expect(axiosMock.history.post.length).toEqual(1);
|
||||||
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
||||||
});
|
});
|
||||||
@@ -137,7 +120,10 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
|||||||
const ignoreConfirmBtn = await screen.findByRole('button', { name: 'Ignore' });
|
const ignoreConfirmBtn = await screen.findByRole('button', { name: 'Ignore' });
|
||||||
userEvent.click(ignoreConfirmBtn);
|
userEvent.click(ignoreConfirmBtn);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
|
expect(mockSendMessageToIframe).toHaveBeenCalledWith(
|
||||||
|
messageTypes.completeXBlockEditing,
|
||||||
|
{ locator: usageKey },
|
||||||
|
);
|
||||||
expect(axiosMock.history.delete.length).toEqual(1);
|
expect(axiosMock.history.delete.length).toEqual(1);
|
||||||
expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import React, { useCallback, useContext, useState } from 'react';
|
import { useCallback, useContext, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActionRow, Button, ModalDialog, useToggle,
|
ActionRow, Button, ModalDialog, useToggle,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
|
import { Warning } from '@openedx/paragon/icons';
|
||||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { useEventListener } from '../../generic/hooks';
|
import { useEventListener } from '../../generic/hooks';
|
||||||
import { messageTypes } from '../constants';
|
import { messageTypes } from '../constants';
|
||||||
import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget';
|
import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget';
|
||||||
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks';
|
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks';
|
||||||
|
import AlertMessage from '../../generic/alert-message';
|
||||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { ToastContext } from '../../generic/toast-context';
|
import { ToastContext } from '../../generic/toast-context';
|
||||||
import LoadingButton from '../../generic/loading-button';
|
import LoadingButton from '../../generic/loading-button';
|
||||||
import Loading from '../../generic/Loading';
|
import Loading from '../../generic/Loading';
|
||||||
import { useContainer, useLibraryBlockMetadata } from '../../library-authoring/data/apiHooks';
|
|
||||||
|
|
||||||
export interface LibraryChangesMessageData {
|
export interface LibraryChangesMessageData {
|
||||||
displayName: string,
|
displayName: string,
|
||||||
@@ -25,11 +26,10 @@ export interface LibraryChangesMessageData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PreviewLibraryXBlockChangesProps {
|
export interface PreviewLibraryXBlockChangesProps {
|
||||||
blockData?: LibraryChangesMessageData,
|
blockData: LibraryChangesMessageData,
|
||||||
isModalOpen: boolean,
|
isModalOpen: boolean,
|
||||||
closeModal: () => void,
|
closeModal: () => void,
|
||||||
postChange: (accept: boolean) => void,
|
postChange: (accept: boolean) => void,
|
||||||
alertNode?: React.ReactNode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,6 @@ export const PreviewLibraryXBlockChanges = ({
|
|||||||
isModalOpen,
|
isModalOpen,
|
||||||
closeModal,
|
closeModal,
|
||||||
postChange,
|
postChange,
|
||||||
alertNode,
|
|
||||||
}: PreviewLibraryXBlockChangesProps) => {
|
}: PreviewLibraryXBlockChangesProps) => {
|
||||||
const { showToast } = useContext(ToastContext);
|
const { showToast } = useContext(ToastContext);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -49,32 +48,9 @@ export const PreviewLibraryXBlockChanges = ({
|
|||||||
// ignore changes confirmation modal toggle.
|
// ignore changes confirmation modal toggle.
|
||||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
||||||
|
|
||||||
// TODO: Split into two different components to avoid making these two calls in which
|
|
||||||
// one will definitely fail
|
|
||||||
const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId);
|
|
||||||
const { data: unitMetadata } = useContainer(blockData?.upstreamBlockId);
|
|
||||||
|
|
||||||
const metadata = blockData?.isVertical ? unitMetadata : componentMetadata;
|
|
||||||
|
|
||||||
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
||||||
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
||||||
|
|
||||||
const getTitle = useCallback(() => {
|
|
||||||
const oldName = blockData?.displayName;
|
|
||||||
const newName = metadata?.displayName;
|
|
||||||
|
|
||||||
if (!oldName) {
|
|
||||||
if (blockData?.isVertical) {
|
|
||||||
return intl.formatMessage(messages.defaultUnitTitle);
|
|
||||||
}
|
|
||||||
return intl.formatMessage(messages.defaultComponentTitle);
|
|
||||||
}
|
|
||||||
if (oldName === newName || !newName) {
|
|
||||||
return intl.formatMessage(messages.title, { blockTitle: oldName });
|
|
||||||
}
|
|
||||||
return intl.formatMessage(messages.diffTitle, { oldName, newName });
|
|
||||||
}, [blockData, metadata]);
|
|
||||||
|
|
||||||
const getBody = useCallback(() => {
|
const getBody = useCallback(() => {
|
||||||
if (!blockData) {
|
if (!blockData) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
@@ -108,12 +84,21 @@ export const PreviewLibraryXBlockChanges = ({
|
|||||||
}
|
}
|
||||||
}, [blockData]);
|
}, [blockData]);
|
||||||
|
|
||||||
|
const defaultTitle = intl.formatMessage(
|
||||||
|
blockData.isVertical
|
||||||
|
? messages.defaultUnitTitle
|
||||||
|
: messages.defaultComponentTitle,
|
||||||
|
);
|
||||||
|
const title = blockData.displayName
|
||||||
|
? intl.formatMessage(messages.title, { blockTitle: blockData?.displayName })
|
||||||
|
: defaultTitle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalDialog
|
<ModalDialog
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
size="xl"
|
size="xl"
|
||||||
title={getTitle()}
|
title={title}
|
||||||
className="lib-preview-xblock-changes-modal"
|
className="lib-preview-xblock-changes-modal"
|
||||||
hasCloseButton
|
hasCloseButton
|
||||||
isFullscreenOnMobile
|
isFullscreenOnMobile
|
||||||
@@ -121,11 +106,16 @@ export const PreviewLibraryXBlockChanges = ({
|
|||||||
>
|
>
|
||||||
<ModalDialog.Header>
|
<ModalDialog.Header>
|
||||||
<ModalDialog.Title>
|
<ModalDialog.Title>
|
||||||
{getTitle()}
|
{title}
|
||||||
</ModalDialog.Title>
|
</ModalDialog.Title>
|
||||||
</ModalDialog.Header>
|
</ModalDialog.Header>
|
||||||
<ModalDialog.Body>
|
<ModalDialog.Body>
|
||||||
{alertNode}
|
<AlertMessage
|
||||||
|
show
|
||||||
|
variant="warning"
|
||||||
|
icon={Warning}
|
||||||
|
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
|
||||||
|
/>
|
||||||
{getBody()}
|
{getBody()}
|
||||||
</ModalDialog.Body>
|
</ModalDialog.Body>
|
||||||
<ModalDialog.Footer>
|
<ModalDialog.Footer>
|
||||||
@@ -186,12 +176,18 @@ const IframePreviewLibraryXBlockChanges = () => {
|
|||||||
|
|
||||||
useEventListener('message', receiveMessage);
|
useEventListener('message', receiveMessage);
|
||||||
|
|
||||||
|
if (!blockData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockPayload = { locator: blockData.downstreamBlockId };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreviewLibraryXBlockChanges
|
<PreviewLibraryXBlockChanges
|
||||||
blockData={blockData}
|
blockData={blockData}
|
||||||
isModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
closeModal={closeModal}
|
closeModal={closeModal}
|
||||||
postChange={() => sendMessageToIframe(messageTypes.refreshXBlock, null)}
|
postChange={() => sendMessageToIframe(messageTypes.completeXBlockEditing, blockPayload)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Preview changes: {blockTitle}',
|
defaultMessage: 'Preview changes: {blockTitle}',
|
||||||
description: 'Preview changes modal title text',
|
description: 'Preview changes modal title text',
|
||||||
},
|
},
|
||||||
diffTitle: {
|
|
||||||
id: 'authoring.course-unit.preview-changes.modal-diff-title',
|
|
||||||
defaultMessage: 'Preview changes: {oldName} -> {newName}',
|
|
||||||
description: 'Preview changes modal title text',
|
|
||||||
},
|
|
||||||
defaultUnitTitle: {
|
defaultUnitTitle: {
|
||||||
id: 'authoring.course-unit.preview-changes.modal-default-unit-title',
|
id: 'authoring.course-unit.preview-changes.modal-default-unit-title',
|
||||||
defaultMessage: 'Preview changes: Unit',
|
defaultMessage: 'Preview changes: Unit',
|
||||||
@@ -61,6 +56,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Ignore',
|
defaultMessage: 'Ignore',
|
||||||
description: 'Preview changes confirmation dialog confirm button text when user clicks on ignore changes.',
|
description: 'Preview changes confirmation dialog confirm button text when user clicks on ignore changes.',
|
||||||
},
|
},
|
||||||
|
olderVersionPreviewAlert: {
|
||||||
|
id: 'course-authoring.review-tab.preview.old-version-alert',
|
||||||
|
defaultMessage: 'The old version preview is the previous library version',
|
||||||
|
description: 'Alert message stating that older version in preview is of library block',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import initializeStore from '../../../../store';
|
import initializeStore from '../../../../store';
|
||||||
import { executeThunk } from '../../../../utils';
|
import { executeThunk } from '../../../../utils';
|
||||||
import { clipboardUnit } from '../../../../__mocks__';
|
import { clipboardUnit } from '../../../../__mocks__';
|
||||||
import { getCourseUnitApiUrl } from '../../../data/api';
|
import { getCourseSectionVerticalApiUrl } from '../../../data/api';
|
||||||
import { getClipboardUrl } from '../../../../generic/data/api';
|
import { getClipboardUrl } from '../../../../generic/data/api';
|
||||||
import { fetchCourseUnitQuery } from '../../../data/thunk';
|
import { fetchCourseSectionVerticalData } from '../../../data/thunk';
|
||||||
import { courseUnitIndexMock } from '../../../__mocks__';
|
import { courseSectionVerticalMock } from '../../../__mocks__';
|
||||||
import messages from '../../messages';
|
import messages from '../../messages';
|
||||||
import ActionButtons from './ActionButtons';
|
import ActionButtons from './ActionButtons';
|
||||||
|
|
||||||
@@ -46,8 +46,14 @@ describe('<ActionButtons />', () => {
|
|||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseUnitApiUrl(courseId))
|
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||||
.reply(200, { ...courseUnitIndexMock, enable_copy_paste_units: true });
|
.reply(200, {
|
||||||
|
...courseSectionVerticalMock,
|
||||||
|
xblock_info: {
|
||||||
|
...courseSectionVerticalMock.xblock_info,
|
||||||
|
enable_copy_paste_units: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
axiosMock
|
axiosMock
|
||||||
.onPost(getClipboardUrl())
|
.onPost(getClipboardUrl())
|
||||||
.reply(200, clipboardUnit);
|
.reply(200, clipboardUnit);
|
||||||
@@ -57,7 +63,7 @@ describe('<ActionButtons />', () => {
|
|||||||
|
|
||||||
queryClient = new QueryClient();
|
queryClient = new QueryClient();
|
||||||
|
|
||||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render ActionButtons component with Copy to clipboard', () => {
|
it('render ActionButtons component with Copy to clipboard', () => {
|
||||||
@@ -74,7 +80,9 @@ describe('<ActionButtons />', () => {
|
|||||||
|
|
||||||
userEvent.click(copyXBlockBtn);
|
userEvent.click(copyXBlockBtn);
|
||||||
expect(axiosMock.history.post.length).toBe(1);
|
expect(axiosMock.history.post.length).toBe(1);
|
||||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({ usage_key: courseUnitIndexMock.id }));
|
expect(axiosMock.history.post[0].data).toBe(
|
||||||
|
JSON.stringify({ usage_key: courseSectionVerticalMock.xblock_info.id }),
|
||||||
|
);
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,4 +99,4 @@ export const getIconVariant = (visibilityState, published, hasChanges) => {
|
|||||||
* @param {string} id - The course unit ID.
|
* @param {string} id - The course unit ID.
|
||||||
* @returns {string} The clear course unit ID extracted from the provided data.
|
* @returns {string} The clear course unit ID extracted from the provided data.
|
||||||
*/
|
*/
|
||||||
export const extractCourseUnitId = (id) => id.match(/block@(.+)$/)[1];
|
export const extractCourseUnitId = (id) => id?.match(/block@(.+)$/)[1];
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export type UseMessageHandlersTypes = {
|
export type UseMessageHandlersTypes = {
|
||||||
courseId: string;
|
courseId: string;
|
||||||
navigate: (path: string) => void;
|
|
||||||
dispatch: (action: any) => void;
|
dispatch: (action: any) => void;
|
||||||
setIframeOffset: (height: number) => void;
|
setIframeOffset: (height: number) => void;
|
||||||
handleDeleteXBlock: (usageId: string) => void;
|
handleDeleteXBlock: (usageId: string) => void;
|
||||||
handleScrollToXBlock: (scrollOffset: number) => void;
|
handleScrollToXBlock: (scrollOffset: number) => void;
|
||||||
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
|
handleDuplicateXBlock: (usageId: string) => void;
|
||||||
|
handleEditXBlock: (blockType: string, usageId: string) => void;
|
||||||
handleManageXBlockAccess: (usageId: string) => void;
|
handleManageXBlockAccess: (usageId: string) => void;
|
||||||
handleShowLegacyEditXBlockModal: (id: string) => void;
|
handleShowLegacyEditXBlockModal: (id: string) => void;
|
||||||
handleCloseLegacyEditorXBlockModal: () => void;
|
handleCloseLegacyEditorXBlockModal: () => void;
|
||||||
@@ -14,7 +14,6 @@ export type UseMessageHandlersTypes = {
|
|||||||
handleOpenManageTagsModal: (id: string) => void;
|
handleOpenManageTagsModal: (id: string) => void;
|
||||||
handleShowProcessingNotification: (variant: string) => void;
|
handleShowProcessingNotification: (variant: string) => void;
|
||||||
handleHideProcessingNotification: () => void;
|
handleHideProcessingNotification: () => void;
|
||||||
handleRedirectToXBlockEditPage: (payload: { type: string, locator: string }) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageHandlersTypes = Record<string, (payload: any) => void>;
|
export type MessageHandlersTypes = Record<string, (payload: any) => void>;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
|
|||||||
*/
|
*/
|
||||||
export const useMessageHandlers = ({
|
export const useMessageHandlers = ({
|
||||||
courseId,
|
courseId,
|
||||||
navigate,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
setIframeOffset,
|
setIframeOffset,
|
||||||
handleDeleteXBlock,
|
handleDeleteXBlock,
|
||||||
@@ -30,15 +29,15 @@ export const useMessageHandlers = ({
|
|||||||
handleOpenManageTagsModal,
|
handleOpenManageTagsModal,
|
||||||
handleShowProcessingNotification,
|
handleShowProcessingNotification,
|
||||||
handleHideProcessingNotification,
|
handleHideProcessingNotification,
|
||||||
handleRedirectToXBlockEditPage,
|
handleEditXBlock,
|
||||||
}: UseMessageHandlersTypes): MessageHandlersTypes => {
|
}: UseMessageHandlersTypes): MessageHandlersTypes => {
|
||||||
const { copyToClipboard } = useClipboard();
|
const { copyToClipboard } = useClipboard();
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
|
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
|
||||||
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
|
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
|
||||||
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
|
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
|
||||||
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
|
[messageTypes.duplicateXBlock]: ({ usageId }) => handleDuplicateXBlock(usageId),
|
||||||
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
|
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
|
||||||
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
|
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
|
||||||
[messageTypes.toggleCourseXBlockDropdown]: ({
|
[messageTypes.toggleCourseXBlockDropdown]: ({
|
||||||
@@ -52,9 +51,14 @@ export const useMessageHandlers = ({
|
|||||||
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
|
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
|
||||||
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
|
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
|
||||||
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
|
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
|
||||||
[messageTypes.copyXBlockLegacy]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.copying),
|
[messageTypes.copyXBlockLegacy]: /* istanbul ignore next */ () => handleShowProcessingNotification(
|
||||||
|
NOTIFICATION_MESSAGES.copying,
|
||||||
|
),
|
||||||
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
|
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
|
||||||
[messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
|
[messageTypes.handleRedirectToXBlockEditPage]: /* istanbul ignore next */ (payload) => handleEditXBlock(
|
||||||
|
payload.type,
|
||||||
|
payload.locator,
|
||||||
|
),
|
||||||
}), [
|
}), [
|
||||||
courseId,
|
courseId,
|
||||||
handleDeleteXBlock,
|
handleDeleteXBlock,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import {
|
import {
|
||||||
FC, useEffect, useState, useMemo, useCallback,
|
FC, useEffect, useState, useMemo, useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { useToggle, Sheet } from '@openedx/paragon';
|
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hideProcessingNotification,
|
hideProcessingNotification,
|
||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||||
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
|
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
|
||||||
import ModalIframe from '../../generic/modal-iframe';
|
import ModalIframe from '../../generic/modal-iframe';
|
||||||
|
import { getWaffleFlags } from '../../data/selectors';
|
||||||
import { IFRAME_FEATURE_POLICY } from '../../constants';
|
import { IFRAME_FEATURE_POLICY } from '../../constants';
|
||||||
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
|
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
|
||||||
import supportedEditors from '../../editors/supportedEditors';
|
|
||||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||||
import {
|
import {
|
||||||
fetchCourseSectionVerticalData,
|
fetchCourseSectionVerticalData,
|
||||||
@@ -35,16 +35,29 @@ import messages from './messages';
|
|||||||
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
|
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
|
||||||
import { useIframeContent } from '../../generic/hooks/useIframeContent';
|
import { useIframeContent } from '../../generic/hooks/useIframeContent';
|
||||||
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
|
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
|
||||||
|
import VideoSelectorPage from '../../editors/VideoSelectorPage';
|
||||||
|
import EditorPage from '../../editors/EditorPage';
|
||||||
|
import { RequestStatus } from '../../data/constants';
|
||||||
|
|
||||||
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||||
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
|
courseId,
|
||||||
|
blockId,
|
||||||
|
unitXBlockActions,
|
||||||
|
courseVerticalChildren,
|
||||||
|
handleConfigureSubmit,
|
||||||
|
isUnitVerticalType,
|
||||||
|
courseUnitLoadingStatus,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||||
|
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
|
||||||
|
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
|
||||||
|
const [blockType, setBlockType] = useState<string>('');
|
||||||
|
const { useVideoGalleryFlow, useReactMarkdownEditor } = useSelector(getWaffleFlags);
|
||||||
|
const [newBlockId, setNewBlockId] = useState<string>('');
|
||||||
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
|
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
|
||||||
const [iframeOffset, setIframeOffset] = useState(0);
|
const [iframeOffset, setIframeOffset] = useState(0);
|
||||||
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
|
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
|
||||||
@@ -64,14 +77,44 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
|||||||
setIframeRef(iframeRef);
|
setIframeRef(iframeRef);
|
||||||
}, [setIframeRef]);
|
}, [setIframeRef]);
|
||||||
|
|
||||||
const handleDuplicateXBlock = useCallback(
|
useEffect(() => {
|
||||||
(blockType: string, usageId: string) => {
|
const iframe = iframeRef?.current;
|
||||||
unitXBlockActions.handleDuplicate(usageId);
|
if (!iframe) { return undefined; }
|
||||||
if (supportedEditors[blockType]) {
|
|
||||||
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
|
const handleIframeLoad = () => {
|
||||||
|
if (courseUnitLoadingStatus.fetchUnitLoadingStatus === RequestStatus.FAILED) {
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
iframe.addEventListener('load', handleIframeLoad);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
iframe.removeEventListener('load', handleIframeLoad);
|
||||||
|
};
|
||||||
|
}, [iframeRef]);
|
||||||
|
|
||||||
|
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
|
||||||
|
closeXBlockEditorModal();
|
||||||
|
closeVideoSelectorModal();
|
||||||
|
sendMessageToIframe(messageTypes.refreshXBlock, null);
|
||||||
|
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
|
||||||
|
|
||||||
|
const handleEditXBlock = useCallback((type: string, id: string) => {
|
||||||
|
setBlockType(type);
|
||||||
|
setNewBlockId(id);
|
||||||
|
if (type === 'video' && useVideoGalleryFlow) {
|
||||||
|
showVideoSelectorModal();
|
||||||
|
} else {
|
||||||
|
showXBlockEditorModal();
|
||||||
|
}
|
||||||
|
}, [showVideoSelectorModal, showXBlockEditorModal]);
|
||||||
|
|
||||||
|
const handleDuplicateXBlock = useCallback(
|
||||||
|
(usageId: string) => {
|
||||||
|
unitXBlockActions.handleDuplicate(usageId);
|
||||||
},
|
},
|
||||||
[unitXBlockActions, courseId, navigate],
|
[unitXBlockActions, courseId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteXBlock = (usageId: string) => {
|
const handleDeleteXBlock = (usageId: string) => {
|
||||||
@@ -147,13 +190,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
|||||||
dispatch(hideProcessingNotification());
|
dispatch(hideProcessingNotification());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
|
|
||||||
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const messageHandlers = useMessageHandlers({
|
const messageHandlers = useMessageHandlers({
|
||||||
courseId,
|
courseId,
|
||||||
navigate,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
setIframeOffset,
|
setIframeOffset,
|
||||||
handleDeleteXBlock,
|
handleDeleteXBlock,
|
||||||
@@ -167,7 +205,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
|||||||
handleOpenManageTagsModal,
|
handleOpenManageTagsModal,
|
||||||
handleShowProcessingNotification,
|
handleShowProcessingNotification,
|
||||||
handleHideProcessingNotification,
|
handleHideProcessingNotification,
|
||||||
handleRedirectToXBlockEditPage,
|
handleEditXBlock,
|
||||||
});
|
});
|
||||||
|
|
||||||
useIframeMessages(messageHandlers);
|
useIframeMessages(messageHandlers);
|
||||||
@@ -186,6 +224,38 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
|||||||
close={closeDeleteModal}
|
close={closeDeleteModal}
|
||||||
onDeleteSubmit={onDeleteSubmit}
|
onDeleteSubmit={onDeleteSubmit}
|
||||||
/>
|
/>
|
||||||
|
<StandardModal
|
||||||
|
title={intl.formatMessage(messages.videoPickerModalTitle)}
|
||||||
|
isOpen={isVideoSelectorModalOpen}
|
||||||
|
onClose={closeVideoSelectorModal}
|
||||||
|
isOverflowVisible={false}
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
<div className="selector-page">
|
||||||
|
<VideoSelectorPage
|
||||||
|
blockId={newBlockId}
|
||||||
|
courseId={courseId}
|
||||||
|
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
|
||||||
|
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||||
|
onCancel={closeVideoSelectorModal}
|
||||||
|
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StandardModal>
|
||||||
|
{isXBlockEditorModalOpen && (
|
||||||
|
<div className="editor-page">
|
||||||
|
<EditorPage
|
||||||
|
courseId={courseId}
|
||||||
|
blockType={blockType}
|
||||||
|
blockId={newBlockId}
|
||||||
|
isMarkdownEditorEnabledForCourse={useReactMarkdownEditor}
|
||||||
|
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
|
||||||
|
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||||
|
onClose={closeXBlockEditorModal}
|
||||||
|
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{Object.keys(accessManagedXBlockData).length ? (
|
{Object.keys(accessManagedXBlockData).length ? (
|
||||||
<ConfigureModal
|
<ConfigureModal
|
||||||
isXBlockComponent
|
isXBlockComponent
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.course-unit.xblock.iframe.label',
|
id: 'course-authoring.course-unit.xblock.iframe.label',
|
||||||
defaultMessage: '{xblockCount} xBlocks inside the frame',
|
defaultMessage: '{xblockCount} xBlocks inside the frame',
|
||||||
},
|
},
|
||||||
|
videoPickerModalTitle: {
|
||||||
|
id: 'course-authoring.course-unit.xblock.video-editor.title',
|
||||||
|
defaultMessage: 'Select video',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ export interface XBlockContainerIframeProps {
|
|||||||
courseId: string;
|
courseId: string;
|
||||||
blockId: string;
|
blockId: string;
|
||||||
isUnitVerticalType: boolean,
|
isUnitVerticalType: boolean,
|
||||||
|
courseUnitLoadingStatus: {
|
||||||
|
fetchUnitLoadingStatus: string;
|
||||||
|
fetchVerticalChildrenLoadingStatus: string;
|
||||||
|
fetchXBlockDataLoadingStatus: string;
|
||||||
|
};
|
||||||
unitXBlockActions: {
|
unitXBlockActions: {
|
||||||
handleDelete: (XBlockId: string | null) => void;
|
handleDelete: (XBlockId: string | null) => void;
|
||||||
handleDuplicate: (XBlockId: string | null) => void;
|
handleDuplicate: (XBlockId: string | null) => void;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const slice = createSlice({
|
|||||||
useNewCertificatesPage: true,
|
useNewCertificatesPage: true,
|
||||||
useNewTextbooksPage: true,
|
useNewTextbooksPage: true,
|
||||||
useNewGroupConfigurationsPage: true,
|
useNewGroupConfigurationsPage: true,
|
||||||
|
useVideoGalleryFlow: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import * as hooks from './hooks';
|
|||||||
|
|
||||||
import supportedEditors from './supportedEditors';
|
import supportedEditors from './supportedEditors';
|
||||||
import type { EditorComponent } from './EditorComponent';
|
import type { EditorComponent } from './EditorComponent';
|
||||||
import { useEditorContext } from './EditorContext';
|
|
||||||
import AdvancedEditor from './AdvancedEditor';
|
import AdvancedEditor from './AdvancedEditor';
|
||||||
|
|
||||||
export interface Props extends EditorComponent {
|
export interface Props extends EditorComponent {
|
||||||
@@ -17,7 +16,6 @@ export interface Props extends EditorComponent {
|
|||||||
learningContextId: string | null;
|
learningContextId: string | null;
|
||||||
lmsEndpointUrl: string | null;
|
lmsEndpointUrl: string | null;
|
||||||
studioEndpointUrl: string | null;
|
studioEndpointUrl: string | null;
|
||||||
fullScreen?: boolean; // eslint-disable-line react/no-unused-prop-types
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Editor: React.FC<Props> = ({
|
const Editor: React.FC<Props> = ({
|
||||||
@@ -42,7 +40,6 @@ const Editor: React.FC<Props> = ({
|
|||||||
studioEndpointUrl,
|
studioEndpointUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { fullScreen } = useEditorContext();
|
|
||||||
|
|
||||||
const EditorComponent = supportedEditors[blockType];
|
const EditorComponent = supportedEditors[blockType];
|
||||||
|
|
||||||
@@ -60,24 +57,7 @@ const Editor: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const innerEditor = <EditorComponent {...{ onClose, returnFunction }} />;
|
return <EditorComponent {...{ onClose, returnFunction }} />;
|
||||||
|
|
||||||
if (fullScreen) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="d-flex flex-column"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="pgn__modal-fullscreen h-100"
|
|
||||||
role="dialog"
|
|
||||||
aria-label={blockType}
|
|
||||||
>
|
|
||||||
{innerEditor}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return innerEditor;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import React from 'react';
|
|||||||
*/
|
*/
|
||||||
export interface EditorContext {
|
export interface EditorContext {
|
||||||
learningContextId: string;
|
learningContextId: string;
|
||||||
/**
|
|
||||||
* When editing components in the libraries part of the Authoring MFE, we show
|
|
||||||
* the editors in a modal (fullScreen = false). This is the preferred approach
|
|
||||||
* so that authors can see context behind the modal.
|
|
||||||
* However, when making edits from the legacy course view, we display the
|
|
||||||
* editors in a fullscreen view. This approach is deprecated.
|
|
||||||
*/
|
|
||||||
fullScreen: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = React.createContext<EditorContext | undefined>(undefined);
|
const context = React.createContext<EditorContext | undefined>(undefined);
|
||||||
@@ -32,7 +24,6 @@ export function useEditorContext() {
|
|||||||
export const EditorContextProvider: React.FC<{
|
export const EditorContextProvider: React.FC<{
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
learningContextId: string;
|
learningContextId: string;
|
||||||
fullScreen: boolean;
|
|
||||||
}> = ({ children, ...contextData }) => {
|
}> = ({ children, ...contextData }) => {
|
||||||
const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []);
|
const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []);
|
||||||
return <context.Provider value={ctx}>{children}</context.Provider>;
|
return <context.Provider value={ctx}>{children}</context.Provider>;
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const defaultPropsHtml = {
|
|||||||
lmsEndpointUrl: 'http://lms.test.none/',
|
lmsEndpointUrl: 'http://lms.test.none/',
|
||||||
studioEndpointUrl: 'http://cms.test.none/',
|
studioEndpointUrl: 'http://cms.test.none/',
|
||||||
onClose: jest.fn(),
|
onClose: jest.fn(),
|
||||||
fullScreen: false,
|
|
||||||
};
|
};
|
||||||
const fieldsHtml = {
|
const fieldsHtml = {
|
||||||
displayName: 'Introduction to Testing',
|
displayName: 'Introduction to Testing',
|
||||||
@@ -66,22 +65,6 @@ describe('EditorPage', () => {
|
|||||||
expect(modalElement.classList).not.toContain('pgn__modal-fullscreen');
|
expect(modalElement.classList).not.toContain('pgn__modal-fullscreen');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it can display the Text (html) editor as a full page (when coming from the legacy UI)', async () => {
|
|
||||||
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => (
|
|
||||||
{ status: 200, data: snakeCaseObject(fieldsHtml) }
|
|
||||||
));
|
|
||||||
|
|
||||||
render(<EditorPage {...defaultPropsHtml} fullScreen />);
|
|
||||||
|
|
||||||
// Then the editor should open
|
|
||||||
expect(await screen.findByRole('heading', { name: /Introduction to Testing/ })).toBeInTheDocument();
|
|
||||||
|
|
||||||
const modalElement = screen.getByRole('dialog');
|
|
||||||
expect(modalElement.classList).toContain('pgn__modal-fullscreen');
|
|
||||||
expect(modalElement.classList).not.toContain('pgn__modal');
|
|
||||||
expect(modalElement.classList).not.toContain('pgn__modal-xl');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it shows the Advanced Editor if there is no corresponding editor', async () => {
|
test('it shows the Advanced Editor if there is no corresponding editor', async () => {
|
||||||
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => ( // eslint-disable-next-line
|
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => ( // eslint-disable-next-line
|
||||||
{ status: 200, data: { display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '' } }
|
{ status: 200, data: { display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '' } }
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ interface Props extends EditorComponent {
|
|||||||
isMarkdownEditorEnabledForCourse?: boolean;
|
isMarkdownEditorEnabledForCourse?: boolean;
|
||||||
lmsEndpointUrl?: string;
|
lmsEndpointUrl?: string;
|
||||||
studioEndpointUrl?: string;
|
studioEndpointUrl?: string;
|
||||||
fullScreen?: boolean;
|
|
||||||
children?: never;
|
children?: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ const EditorPage: React.FC<Props> = ({
|
|||||||
studioEndpointUrl = null,
|
studioEndpointUrl = null,
|
||||||
onClose = null,
|
onClose = null,
|
||||||
returnFunction = null,
|
returnFunction = null,
|
||||||
fullScreen = true,
|
|
||||||
}) => (
|
}) => (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
@@ -40,7 +38,7 @@ const EditorPage: React.FC<Props> = ({
|
|||||||
studioEndpointUrl,
|
studioEndpointUrl,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditorContextProvider fullScreen={fullScreen} learningContextId={courseId}>
|
<EditorContextProvider learningContextId={courseId}>
|
||||||
<Editor
|
<Editor
|
||||||
{...{
|
{...{
|
||||||
onClose,
|
onClose,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const VideoSelector = ({
|
|||||||
learningContextId,
|
learningContextId,
|
||||||
lmsEndpointUrl,
|
lmsEndpointUrl,
|
||||||
studioEndpointUrl,
|
studioEndpointUrl,
|
||||||
|
returnFunction,
|
||||||
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const loading = hooks.useInitializeApp({
|
const loading = hooks.useInitializeApp({
|
||||||
@@ -26,7 +28,7 @@ const VideoSelector = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<VideoGallery />
|
<VideoGallery returnFunction={returnFunction} onCancel={onCancel} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ VideoSelector.propTypes = {
|
|||||||
learningContextId: PropTypes.string.isRequired,
|
learningContextId: PropTypes.string.isRequired,
|
||||||
lmsEndpointUrl: PropTypes.string.isRequired,
|
lmsEndpointUrl: PropTypes.string.isRequired,
|
||||||
studioEndpointUrl: PropTypes.string.isRequired,
|
studioEndpointUrl: PropTypes.string.isRequired,
|
||||||
|
returnFunction: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoSelector;
|
export default VideoSelector;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const VideoSelectorPage = ({
|
|||||||
courseId,
|
courseId,
|
||||||
lmsEndpointUrl,
|
lmsEndpointUrl,
|
||||||
studioEndpointUrl,
|
studioEndpointUrl,
|
||||||
|
returnFunction,
|
||||||
|
onCancel,
|
||||||
}) => (
|
}) => (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
@@ -24,6 +26,8 @@ const VideoSelectorPage = ({
|
|||||||
learningContextId: courseId,
|
learningContextId: courseId,
|
||||||
lmsEndpointUrl,
|
lmsEndpointUrl,
|
||||||
studioEndpointUrl,
|
studioEndpointUrl,
|
||||||
|
returnFunction,
|
||||||
|
onCancel,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@@ -42,6 +46,8 @@ VideoSelectorPage.propTypes = {
|
|||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
lmsEndpointUrl: PropTypes.string,
|
lmsEndpointUrl: PropTypes.string,
|
||||||
studioEndpointUrl: PropTypes.string,
|
studioEndpointUrl: PropTypes.string,
|
||||||
|
returnFunction: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoSelectorPage;
|
export default VideoSelectorPage;
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const defaultPropsHtml = {
|
|||||||
lmsEndpointUrl: 'http://lms.test.none/',
|
lmsEndpointUrl: 'http://lms.test.none/',
|
||||||
studioEndpointUrl: 'http://cms.test.none/',
|
studioEndpointUrl: 'http://cms.test.none/',
|
||||||
onClose: jest.fn(),
|
onClose: jest.fn(),
|
||||||
fullScreen: false,
|
|
||||||
};
|
};
|
||||||
const fieldsHtml = {
|
const fieldsHtml = {
|
||||||
displayName: 'Introduction to Testing',
|
displayName: 'Introduction to Testing',
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { Close } from '@openedx/paragon/icons';
|
|||||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { EditorComponent } from '../../EditorComponent';
|
import { EditorComponent } from '../../EditorComponent';
|
||||||
import { useEditorContext } from '../../EditorContext';
|
|
||||||
import TitleHeader from './components/TitleHeader';
|
import TitleHeader from './components/TitleHeader';
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -30,37 +29,18 @@ interface WrapperProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => {
|
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => {
|
||||||
const { fullScreen } = useEditorContext();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
if (fullScreen) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="editor-container d-flex flex-column position-relative zindex-0"
|
|
||||||
style={{ minHeight: '100%' }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const title = intl.formatMessage(messages.modalTitle);
|
const title = intl.formatMessage(messages.modalTitle);
|
||||||
return (
|
return (
|
||||||
<ModalDialog isOpen size="xl" isOverflowVisible={false} onClose={onClose} title={title}>{children}</ModalDialog>
|
<ModalDialog isOpen size="xl" isOverflowVisible={false} onClose={onClose} title={title}>{children}</ModalDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => {
|
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => <ModalDialog.Body className="pb-0">{ children }</ModalDialog.Body>;
|
||||||
const { fullScreen } = useEditorContext();
|
|
||||||
return <ModalDialog.Body className={fullScreen ? 'pb-6' : 'pb-0'}>{ children }</ModalDialog.Body>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => {
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
const { fullScreen } = useEditorContext();
|
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => <>{ children }</>;
|
||||||
if (fullScreen) {
|
|
||||||
return <div className="editor-footer fixed-bottom">{children}</div>;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
||||||
return <>{ children }</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props extends EditorComponent {
|
interface Props extends EditorComponent {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('SelectTypeModal', () => {
|
|||||||
jest.spyOn(hooks, 'onSelect').mockImplementation(mockSelect);
|
jest.spyOn(hooks, 'onSelect').mockImplementation(mockSelect);
|
||||||
// This is a new-style test, unlike most of the old snapshot-based editor tests.
|
// This is a new-style test, unlike most of the old snapshot-based editor tests.
|
||||||
render(
|
render(
|
||||||
<EditorContextProvider fullScreen={false} learningContextId="course-v1:Org+COURSE+RUN">
|
<EditorContextProvider learningContextId="course-v1:Org+COURSE+RUN">
|
||||||
<Provider store={editorStore}>
|
<Provider store={editorStore}>
|
||||||
<SelectTypeModal onClose={mockClose} />
|
<SelectTypeModal onClose={mockClose} />
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
|
|||||||
}
|
}
|
||||||
editorType="text"
|
editorType="text"
|
||||||
enableImageUpload={true}
|
enableImageUpload={true}
|
||||||
height="100%"
|
|
||||||
id={null}
|
id={null}
|
||||||
images={{}}
|
images={{}}
|
||||||
initializeEditor={[MockFunction args.intializeEditor]}
|
initializeEditor={[MockFunction args.intializeEditor]}
|
||||||
isLibrary={null}
|
isLibrary={null}
|
||||||
learningContextId="course+org+run"
|
learningContextId="course+org+run"
|
||||||
lmsEndpointUrl=""
|
lmsEndpointUrl=""
|
||||||
|
maxHeight={500}
|
||||||
minHeight={500}
|
minHeight={500}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
|
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
|
||||||
@@ -226,13 +226,13 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
|
|||||||
}
|
}
|
||||||
editorType="text"
|
editorType="text"
|
||||||
enableImageUpload={true}
|
enableImageUpload={true}
|
||||||
height="100%"
|
|
||||||
id={null}
|
id={null}
|
||||||
images={{}}
|
images={{}}
|
||||||
initializeEditor={[MockFunction args.intializeEditor]}
|
initializeEditor={[MockFunction args.intializeEditor]}
|
||||||
isLibrary={null}
|
isLibrary={null}
|
||||||
learningContextId="course+org+run"
|
learningContextId="course+org+run"
|
||||||
lmsEndpointUrl=""
|
lmsEndpointUrl=""
|
||||||
|
maxHeight={500}
|
||||||
minHeight={500}
|
minHeight={500}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
|
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
|
||||||
@@ -292,13 +292,13 @@ exports[`TextEditor snapshots renders static images with relative paths 1`] = `
|
|||||||
}
|
}
|
||||||
editorType="text"
|
editorType="text"
|
||||||
enableImageUpload={true}
|
enableImageUpload={true}
|
||||||
height="100%"
|
|
||||||
id={null}
|
id={null}
|
||||||
images={{}}
|
images={{}}
|
||||||
initializeEditor={[MockFunction args.intializeEditor]}
|
initializeEditor={[MockFunction args.intializeEditor]}
|
||||||
isLibrary={null}
|
isLibrary={null}
|
||||||
learningContextId="course+org+run"
|
learningContextId="course+org+run"
|
||||||
lmsEndpointUrl=""
|
lmsEndpointUrl=""
|
||||||
|
maxHeight={500}
|
||||||
minHeight={500}
|
minHeight={500}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
|
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const TextEditor = ({
|
|||||||
editorContentHtml={editorContent}
|
editorContentHtml={editorContent}
|
||||||
setEditorRef={setEditorRef}
|
setEditorRef={setEditorRef}
|
||||||
minHeight={500}
|
minHeight={500}
|
||||||
height="100%"
|
maxHeight={500}
|
||||||
initializeEditor={initializeEditor}
|
initializeEditor={initializeEditor}
|
||||||
{...{
|
{...{
|
||||||
images,
|
images,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ exports[`VideoEditor snapshots renders as expected with default behavior 1`] = `
|
|||||||
"useSelector": [MockFunction],
|
"useSelector": [MockFunction],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onClose={[MockFunction props.onClose]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EditorContainer>
|
</EditorContainer>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import VideoSettingsModal from './VideoSettingsModal';
|
|||||||
import { RequestKeys } from '../../../data/constants/requests';
|
import { RequestKeys } from '../../../data/constants/requests';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
onReturn?: (() => void);
|
||||||
isLibrary: boolean;
|
isLibrary: boolean;
|
||||||
|
onClose?: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
@@ -27,13 +29,15 @@ export const hooks = {
|
|||||||
|
|
||||||
const VideoEditorModal: React.FC<Props> = ({
|
const VideoEditorModal: React.FC<Props> = ({
|
||||||
isLibrary,
|
isLibrary,
|
||||||
|
onClose,
|
||||||
|
onReturn,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const searchParams = new URLSearchParams(location.search);
|
const searchParams = new URLSearchParams(location.search);
|
||||||
const selectedVideoId = searchParams.get('selectedVideoId');
|
const selectedVideoId = searchParams.get('selectedVideoId');
|
||||||
const selectedVideoUrl = searchParams.get('selectedVideoUrl');
|
const selectedVideoUrl = searchParams.get('selectedVideoUrl');
|
||||||
const onReturn = hooks.useReturnToGallery();
|
const onSettingsReturn = onReturn || hooks.useReturnToGallery();
|
||||||
const isLoaded = useSelector(
|
const isLoaded = useSelector(
|
||||||
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
||||||
);
|
);
|
||||||
@@ -44,8 +48,9 @@ const VideoEditorModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<VideoSettingsModal {...{
|
<VideoSettingsModal {...{
|
||||||
onReturn,
|
onReturn: onSettingsReturn,
|
||||||
isLibrary,
|
isLibrary,
|
||||||
|
onClose,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ import messages from '../../messages';
|
|||||||
interface Props {
|
interface Props {
|
||||||
onReturn: () => void;
|
onReturn: () => void;
|
||||||
isLibrary: boolean;
|
isLibrary: boolean;
|
||||||
|
onClose?: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoSettingsModal: React.FC<Props> = ({
|
const VideoSettingsModal: React.FC<Props> = ({
|
||||||
onReturn,
|
onReturn,
|
||||||
isLibrary,
|
isLibrary,
|
||||||
|
onClose,
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
{!isLibrary && (
|
{!isLibrary && (
|
||||||
@@ -32,7 +34,7 @@ const VideoSettingsModal: React.FC<Props> = ({
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="text-primary-500"
|
className="text-primary-500"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onReturn}
|
onClick={onClose || onReturn}
|
||||||
style={{
|
style={{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
marginLeft: '3px',
|
marginLeft: '3px',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const VideoEditor: React.FC<EditorComponent> = ({
|
|||||||
>
|
>
|
||||||
{(isCreateWorkflow || studioViewFinished) ? (
|
{(isCreateWorkflow || studioViewFinished) ? (
|
||||||
<div className="video-editor">
|
<div className="video-editor">
|
||||||
<VideoEditorModal {...{ isLibrary }} />
|
<VideoEditorModal {...{ isLibrary, onClose, returnFunction }} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const filterList = ({
|
|||||||
export const useVideoListProps = ({
|
export const useVideoListProps = ({
|
||||||
searchSortProps,
|
searchSortProps,
|
||||||
videos,
|
videos,
|
||||||
|
returnFunction,
|
||||||
}) => {
|
}) => {
|
||||||
const [highlighted, setHighlighted] = React.useState(null);
|
const [highlighted, setHighlighted] = React.useState(null);
|
||||||
const [
|
const [
|
||||||
@@ -128,7 +129,10 @@ export const useVideoListProps = ({
|
|||||||
},
|
},
|
||||||
selectBtnProps: {
|
selectBtnProps: {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (highlighted) {
|
/* istanbul ignore next */
|
||||||
|
if (returnFunction) {
|
||||||
|
returnFunction()();
|
||||||
|
} else if (highlighted) {
|
||||||
navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`);
|
navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`);
|
||||||
} else {
|
} else {
|
||||||
setShowSelectVideoError(true);
|
setShowSelectVideoError(true);
|
||||||
@@ -138,10 +142,15 @@ export const useVideoListProps = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useVideoUploadHandler = ({ replace }) => {
|
export const useVideoUploadHandler = ({ replace, uploadHandler }) => {
|
||||||
const learningContextId = useSelector(selectors.app.learningContextId);
|
const learningContextId = useSelector(selectors.app.learningContextId);
|
||||||
const blockId = useSelector(selectors.app.blockId);
|
const blockId = useSelector(selectors.app.blockId);
|
||||||
const path = `/course/${learningContextId}/editor/video_upload/${blockId}`;
|
const path = `/course/${learningContextId}/editor/video_upload/${blockId}`;
|
||||||
|
if (uploadHandler) {
|
||||||
|
return () => {
|
||||||
|
uploadHandler();
|
||||||
|
};
|
||||||
|
}
|
||||||
if (replace) {
|
if (replace) {
|
||||||
return () => window.location.replace(path);
|
return () => window.location.replace(path);
|
||||||
}
|
}
|
||||||
@@ -191,11 +200,12 @@ export const getstatusBadgeVariant = ({ status }) => {
|
|||||||
|
|
||||||
export const getStatusMessage = ({ status }) => Object.values(filterMessages).find((m) => m.defaultMessage === status);
|
export const getStatusMessage = ({ status }) => Object.values(filterMessages).find((m) => m.defaultMessage === status);
|
||||||
|
|
||||||
export const useVideoProps = ({ videos }) => {
|
export const useVideoProps = ({ videos, uploadHandler, returnFunction }) => {
|
||||||
const searchSortProps = useSearchAndSortProps();
|
const searchSortProps = useSearchAndSortProps();
|
||||||
const videoList = useVideoListProps({
|
const videoList = useVideoListProps({
|
||||||
searchSortProps,
|
searchSortProps,
|
||||||
videos,
|
videos,
|
||||||
|
returnFunction,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
galleryError,
|
galleryError,
|
||||||
@@ -203,7 +213,7 @@ export const useVideoProps = ({ videos }) => {
|
|||||||
inputError,
|
inputError,
|
||||||
selectBtnProps,
|
selectBtnProps,
|
||||||
} = videoList;
|
} = videoList;
|
||||||
const fileInput = { click: useVideoUploadHandler({ replace: false }) };
|
const fileInput = { click: useVideoUploadHandler({ replace: false, uploadHandler }) };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
galleryError,
|
galleryError,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Image } from '@openedx/paragon';
|
import PropTypes from 'prop-types';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import {
|
||||||
|
Image, useToggle, StandardModal,
|
||||||
|
} from '@openedx/paragon';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { selectors } from '../../data/redux';
|
import { selectors } from '../../data/redux';
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
@@ -8,8 +13,11 @@ import { acceptedImgKeys } from './utils';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { RequestKeys } from '../../data/constants/requests';
|
import { RequestKeys } from '../../data/constants/requests';
|
||||||
import videoThumbnail from '../../data/images/videoThumbnail.svg';
|
import videoThumbnail from '../../data/images/videoThumbnail.svg';
|
||||||
|
import VideoUploadEditor from '../VideoUploadEditor';
|
||||||
|
import VideoEditor from '../VideoEditor';
|
||||||
|
|
||||||
const VideoGallery = () => {
|
const VideoGallery = ({ returnFunction, onCancel }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const rawVideos = useSelector(selectors.app.videos);
|
const rawVideos = useSelector(selectors.app.videos);
|
||||||
const isLoaded = useSelector(
|
const isLoaded = useSelector(
|
||||||
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
||||||
@@ -21,14 +29,27 @@ const VideoGallery = () => {
|
|||||||
(state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
|
(state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
|
||||||
);
|
);
|
||||||
const videos = hooks.buildVideos({ rawVideos });
|
const videos = hooks.buildVideos({ rawVideos });
|
||||||
const handleVideoUpload = hooks.useVideoUploadHandler({ replace: true });
|
const [isVideoUploadModalOpen, showVideoUploadModal, closeVideoUploadModal] = useToggle();
|
||||||
|
const [isVideoEditorModalOpen, showVideoEditorModal, closeVideoEditorModal] = useToggle();
|
||||||
|
const setSearchParams = useSearchParams()[1];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If no videos exists redirects to the video upload screen
|
// If no videos exists opens to the video upload modal
|
||||||
if (isLoaded && videos.length === 0) {
|
if (isLoaded && videos.length === 0) {
|
||||||
handleVideoUpload();
|
showVideoUploadModal();
|
||||||
}
|
}
|
||||||
}, [isLoaded]);
|
}, [isLoaded]);
|
||||||
|
|
||||||
|
const onVideoUpload = useCallback((videoUrl) => {
|
||||||
|
closeVideoUploadModal();
|
||||||
|
showVideoEditorModal();
|
||||||
|
setSearchParams({ selectedVideoUrl: videoUrl });
|
||||||
|
}, [closeVideoUploadModal, showVideoEditorModal, setSearchParams]);
|
||||||
|
|
||||||
|
const uploadHandler = useCallback(() => {
|
||||||
|
showVideoUploadModal();
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
galleryError,
|
galleryError,
|
||||||
inputError,
|
inputError,
|
||||||
@@ -36,7 +57,7 @@ const VideoGallery = () => {
|
|||||||
galleryProps,
|
galleryProps,
|
||||||
searchSortProps,
|
searchSortProps,
|
||||||
selectBtnProps,
|
selectBtnProps,
|
||||||
} = hooks.useVideoProps({ videos });
|
} = hooks.useVideoProps({ videos, uploadHandler, returnFunction });
|
||||||
const handleCancel = hooks.useCancelHandler();
|
const handleCancel = hooks.useCancelHandler();
|
||||||
|
|
||||||
const modalMessages = {
|
const modalMessages = {
|
||||||
@@ -60,8 +81,8 @@ const VideoGallery = () => {
|
|||||||
<SelectionModal
|
<SelectionModal
|
||||||
{...{
|
{...{
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
close: handleCancel,
|
close: onCancel || handleCancel,
|
||||||
size: 'fullscreen',
|
size: 'xl',
|
||||||
isFullscreenScroll: false,
|
isFullscreenScroll: false,
|
||||||
galleryError,
|
galleryError,
|
||||||
inputError,
|
inputError,
|
||||||
@@ -79,10 +100,34 @@ const VideoGallery = () => {
|
|||||||
isFetchError,
|
isFetchError,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<StandardModal
|
||||||
|
title={intl.formatMessage(messages.videoUploadModalTitle)}
|
||||||
|
isOpen={isVideoUploadModalOpen}
|
||||||
|
onClose={closeVideoUploadModal}
|
||||||
|
isOverflowVisible={false}
|
||||||
|
size="xl"
|
||||||
|
hasCloseButton={false}
|
||||||
|
>
|
||||||
|
<div className="editor-page">
|
||||||
|
<VideoUploadEditor
|
||||||
|
onUpload={onVideoUpload}
|
||||||
|
onClose={closeVideoUploadModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StandardModal>
|
||||||
|
{isVideoEditorModalOpen && (
|
||||||
|
<VideoEditor
|
||||||
|
onClose={closeVideoEditorModal}
|
||||||
|
returnFunction={returnFunction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
VideoGallery.propTypes = {};
|
VideoGallery.propTypes = {
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
returnFunction: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
export default VideoGallery;
|
export default VideoGallery;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import React from 'react';
|
|||||||
import {
|
import {
|
||||||
act, fireEvent, render, screen,
|
act, fireEvent, render, screen,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
|
import * as reactRouterDom from 'react-router-dom';
|
||||||
|
import * as reduxThunks from '../../data/redux';
|
||||||
|
|
||||||
import VideoGallery from './index';
|
import VideoGallery from './index';
|
||||||
|
|
||||||
@@ -120,11 +122,10 @@ describe('VideoGallery', () => {
|
|||||||
expect(screen.getByText(video.client_video_id)).toBeInTheDocument()
|
expect(screen.getByText(video.client_video_id)).toBeInTheDocument()
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
it('navigates to video upload page when there are no videos', async () => {
|
it('renders video upload modal when there are no videos', async () => {
|
||||||
expect(window.location.replace).not.toHaveBeenCalled();
|
|
||||||
updateState({ videos: [] });
|
updateState({ videos: [] });
|
||||||
await renderComponent();
|
await renderComponent();
|
||||||
expect(window.location.replace).toHaveBeenCalled();
|
expect(screen.getByRole('heading', { name: /upload or embed a new video/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it.each([
|
it.each([
|
||||||
[/newest/i, [2, 1, 3]],
|
[/newest/i, [2, 1, 3]],
|
||||||
@@ -191,5 +192,36 @@ describe('VideoGallery', () => {
|
|||||||
expect(screen.queryByText('client_id_1')).not.toBeInTheDocument();
|
expect(screen.queryByText('client_id_1')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('client_id_3')).not.toBeInTheDocument();
|
expect(screen.queryByText('client_id_3')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls onVideoUpload correctly when a video is uploaded', async () => {
|
||||||
|
// Mock useSearchParams
|
||||||
|
const setSearchParams = jest.fn();
|
||||||
|
jest.spyOn(reactRouterDom, 'useSearchParams').mockReturnValue([{}, setSearchParams]);
|
||||||
|
|
||||||
|
// Mock the uploadVideo thunk to immediately call postUploadRedirect
|
||||||
|
jest.spyOn(reduxThunks.thunkActions.video, 'uploadVideo').mockImplementation(
|
||||||
|
({ postUploadRedirect }) => () => {
|
||||||
|
if (postUploadRedirect) {
|
||||||
|
postUploadRedirect('http://test.video/url.mp4');
|
||||||
|
}
|
||||||
|
return { type: 'MOCK_UPLOAD_VIDEO' };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await renderComponent();
|
||||||
|
|
||||||
|
// Open the upload modal by clicking the button
|
||||||
|
const openModalButton = screen.getByRole('button', { name: /upload or embed a new video/i });
|
||||||
|
fireEvent.click(openModalButton);
|
||||||
|
|
||||||
|
// Wait for the input to appear in the modal
|
||||||
|
const urlInput = await screen.findByPlaceholderText('Paste your video ID or URL');
|
||||||
|
fireEvent.change(urlInput, { target: { value: 'http://test.video/url.mp4' } });
|
||||||
|
|
||||||
|
const submitButton = screen.getByRole('button', { name: /submit/i });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(setSearchParams).toHaveBeenCalledWith({ selectedVideoUrl: 'http://test.video/url.mp4' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ const messages = {
|
|||||||
defaultMessage: 'Upload or embed a new video',
|
defaultMessage: 'Upload or embed a new video',
|
||||||
description: 'Label for upload button',
|
description: 'Label for upload button',
|
||||||
},
|
},
|
||||||
|
videoUploadModalTitle: {
|
||||||
|
id: 'authoring.selectvideomodal.upload.title',
|
||||||
|
defaultMessage: 'Upload or embed a new video',
|
||||||
|
description: 'Label for upload modal',
|
||||||
|
},
|
||||||
|
videoEditorModalTitle: {
|
||||||
|
id: 'authoring.selectvideomodal.edit.title',
|
||||||
|
defaultMessage: 'Edit selected video',
|
||||||
|
description: 'Label for editor modal',
|
||||||
|
},
|
||||||
// Sort Dropdown
|
// Sort Dropdown
|
||||||
sortByDateNewest: {
|
sortByDateNewest: {
|
||||||
id: 'authoring.selectvideomodal.sort.datenewest.label',
|
id: 'authoring.selectvideomodal.sort.datenewest.label',
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import { thunkActions } from '../../data/redux';
|
|||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const URLUploader = () => {
|
const URLUploader = ({ onUpload }) => {
|
||||||
const [textInputValue, setTextInputValue] = React.useState('');
|
const [textInputValue, setTextInputValue] = React.useState('');
|
||||||
const onURLUpload = hooks.onVideoUpload('selectedVideoUrl');
|
const onURLUpload = hooks.onVideoUpload('selectedVideoUrl', onUpload);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
@@ -58,16 +58,16 @@ const URLUploader = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VideoUploader = ({ setLoading }) => {
|
export const VideoUploader = ({ setLoading, onUpload, onClose }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const goBack = hooks.useHistoryGoBack();
|
const goBack = onClose || hooks.useHistoryGoBack();
|
||||||
|
|
||||||
const handleProcessUpload = ({ fileData }) => {
|
const handleProcessUpload = ({ fileData }) => {
|
||||||
dispatch(thunkActions.video.uploadVideo({
|
dispatch(thunkActions.video.uploadVideo({
|
||||||
supportedFiles: [fileData],
|
supportedFiles: [fileData],
|
||||||
setLoadSpinner: setLoading,
|
setLoadSpinner: setLoading,
|
||||||
postUploadRedirect: hooks.onVideoUpload('selectedVideoId'),
|
postUploadRedirect: hooks.onVideoUpload('selectedVideoId', onUpload),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,14 +85,20 @@ export const VideoUploader = ({ setLoading }) => {
|
|||||||
<Dropzone
|
<Dropzone
|
||||||
accept={{ 'video/*': ['.mp4', '.mov'] }}
|
accept={{ 'video/*': ['.mp4', '.mov'] }}
|
||||||
onProcessUpload={handleProcessUpload}
|
onProcessUpload={handleProcessUpload}
|
||||||
inputComponent={<URLUploader />}
|
inputComponent={<URLUploader onUpload={onUpload} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
URLUploader.propTypes = {
|
||||||
|
onUpload: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
VideoUploader.propTypes = {
|
VideoUploader.propTypes = {
|
||||||
setLoading: PropTypes.func.isRequired,
|
setLoading: PropTypes.func.isRequired,
|
||||||
|
onUpload: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoUploader;
|
export default VideoUploader;
|
||||||
|
|||||||
@@ -11,15 +11,20 @@ export const {
|
|||||||
navigateTo,
|
navigateTo,
|
||||||
} = appHooks;
|
} = appHooks;
|
||||||
|
|
||||||
export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl') => {
|
export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl', onUpload = null) => {
|
||||||
const learningContextId = selectors.app.learningContextId(storeState);
|
const learningContextId = selectors.app.learningContextId(storeState);
|
||||||
const blockId = selectors.app.blockId(storeState);
|
const blockId = selectors.app.blockId(storeState);
|
||||||
|
if (onUpload) {
|
||||||
|
return (videoUrl) => {
|
||||||
|
onUpload(videoUrl, learningContextId, blockId);
|
||||||
|
};
|
||||||
|
}
|
||||||
return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?${uploadType}=${videoUrl}`);
|
return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?${uploadType}=${videoUrl}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onVideoUpload = (uploadType) => {
|
export const onVideoUpload = (uploadType, onUpload) => {
|
||||||
const storeState = store.getState();
|
const storeState = store.getState();
|
||||||
return module.postUploadRedirect(storeState, uploadType);
|
return module.postUploadRedirect(storeState, uploadType, onUpload);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUploadVideo = async ({
|
export const useUploadVideo = async ({
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Spinner } from '@openedx/paragon';
|
import { Spinner } from '@openedx/paragon';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { VideoUploader } from './VideoUploader';
|
import { VideoUploader } from './VideoUploader';
|
||||||
|
|
||||||
const VideoUploadEditor = () => {
|
const VideoUploadEditor = ({ onUpload, onClose }) => {
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (!loading) ? (
|
return (!loading) ? (
|
||||||
<div className="d-flex marked-area flex-column p-3">
|
<div className="d-flex marked-area flex-column p-3">
|
||||||
<VideoUploader setLoading={setLoading} />
|
<VideoUploader onUpload={onUpload} onClose={onClose} setLoading={setLoading} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -30,4 +31,9 @@ const VideoUploadEditor = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
VideoUploadEditor.propTypes = {
|
||||||
|
onUpload: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
export default VideoUploadEditor;
|
export default VideoUploadEditor;
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ export const saveBlock = (content, returnToUnit) => (dispatch) => {
|
|||||||
content,
|
content,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
dispatch(actions.app.setSaveResponse(response));
|
dispatch(actions.app.setSaveResponse(response));
|
||||||
|
const parsedData = JSON.parse(response.config.data);
|
||||||
|
if (parsedData?.has_changes || !('has_changes' in parsedData)) {
|
||||||
|
const storageKey = 'courseRefreshTriggerOnComponentEditSave';
|
||||||
|
sessionStorage.setItem(storageKey, Date.now());
|
||||||
|
|
||||||
|
window.dispatchEvent(new StorageEvent('storage', {
|
||||||
|
key: storageKey,
|
||||||
|
newValue: Date.now().toString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
returnToUnit(response.data);
|
returnToUnit(response.data);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -352,7 +352,11 @@ describe('app thunkActions', () => {
|
|||||||
});
|
});
|
||||||
it('dispatches actions.app.setSaveResponse with response and then calls returnToUnit', () => {
|
it('dispatches actions.app.setSaveResponse with response and then calls returnToUnit', () => {
|
||||||
dispatch.mockClear();
|
dispatch.mockClear();
|
||||||
const response = 'testRESPONSE';
|
const mockParsedData = { has_changes: true };
|
||||||
|
const response = {
|
||||||
|
config: { data: JSON.stringify(mockParsedData) },
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
calls[1][0].saveBlock.onSuccess(response);
|
calls[1][0].saveBlock.onSuccess(response);
|
||||||
expect(dispatch).toHaveBeenCalledWith(actions.app.setSaveResponse(response));
|
expect(dispatch).toHaveBeenCalledWith(actions.app.setSaveResponse(response));
|
||||||
expect(returnToUnit).toHaveBeenCalled();
|
expect(returnToUnit).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from './problem';
|
} from './problem';
|
||||||
import { checkboxesOLXWithFeedbackAndHintsOLX, advancedProblemOlX, blankProblemOLX } from '../../../containers/ProblemEditor/data/mockData/olxTestData';
|
import { checkboxesOLXWithFeedbackAndHintsOLX, advancedProblemOlX, blankProblemOLX } from '../../../containers/ProblemEditor/data/mockData/olxTestData';
|
||||||
import { ProblemTypeKeys } from '../../constants/problem';
|
import { ProblemTypeKeys } from '../../constants/problem';
|
||||||
import * as requests from './requests';
|
|
||||||
|
|
||||||
const mockOlx = 'SOmEVALue';
|
const mockOlx = 'SOmEVALue';
|
||||||
const mockBuildOlx = jest.fn(() => mockOlx);
|
const mockBuildOlx = jest.fn(() => mockOlx);
|
||||||
@@ -72,22 +71,13 @@ describe('problem thunkActions', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('switchToMarkdownEditor dispatches correct actions', () => {
|
test('switchToMarkdownEditor dispatches correct actions', () => {
|
||||||
switchToMarkdownEditor()(dispatch, getState);
|
switchToMarkdownEditor()(dispatch);
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledWith(
|
expect(dispatch).toHaveBeenCalledWith(
|
||||||
actions.problem.updateField({
|
actions.problem.updateField({
|
||||||
isMarkdownEditorEnabled: true,
|
isMarkdownEditorEnabled: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledWith(
|
|
||||||
requests.saveBlock({
|
|
||||||
content: {
|
|
||||||
settings: { markdown_edited: true },
|
|
||||||
olx: blockValue.data.data,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('switchEditor', () => {
|
describe('switchEditor', () => {
|
||||||
@@ -110,7 +100,7 @@ describe('problem thunkActions', () => {
|
|||||||
|
|
||||||
test('dispatches switchToMarkdownEditor when editorType is markdown', () => {
|
test('dispatches switchToMarkdownEditor when editorType is markdown', () => {
|
||||||
switchEditor('markdown')(dispatch, getState);
|
switchEditor('markdown')(dispatch, getState);
|
||||||
expect(switchToMarkdownEditorMock).toHaveBeenCalledWith(dispatch, getState);
|
expect(switchToMarkdownEditorMock).toHaveBeenCalledWith(dispatch);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ export const switchToAdvancedEditor = () => (dispatch, getState) => {
|
|||||||
dispatch(actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOLX }));
|
dispatch(actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOLX }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const switchToMarkdownEditor = () => (dispatch, getState) => {
|
export const switchToMarkdownEditor = () => (dispatch) => {
|
||||||
const state = getState();
|
|
||||||
dispatch(actions.problem.updateField({ isMarkdownEditorEnabled: true }));
|
dispatch(actions.problem.updateField({ isMarkdownEditorEnabled: true }));
|
||||||
const { blockValue } = state.app;
|
|
||||||
const olx = get(blockValue, 'data.data', '');
|
|
||||||
const content = { settings: { markdown_edited: true }, olx };
|
|
||||||
// Sending a request to save the problem block with the updated markdown_edited value
|
|
||||||
dispatch(requests.saveBlock({ content }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const switchEditor = (editorType) => (dispatch, getState) => (editorType === 'advanced' ? switchToAdvancedEditor : switchToMarkdownEditor)()(dispatch, getState);
|
export const switchEditor = (editorType) => (dispatch, getState) => {
|
||||||
|
if (editorType === 'advanced') {
|
||||||
|
switchToAdvancedEditor()(dispatch, getState);
|
||||||
|
} else {
|
||||||
|
switchToMarkdownEditor()(dispatch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const isBlankProblem = ({ rawOLX }) => {
|
export const isBlankProblem = ({ rawOLX }) => {
|
||||||
if (['<problem></problem>', '<problem/>'].includes(rawOLX.replace(/\s/g, ''))) {
|
if (['<problem></problem>', '<problem/>'].includes(rawOLX.replace(/\s/g, ''))) {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const createCodeMirrorDomNode = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const languageExtension = CODEMIRROR_LANGUAGES[lang]();
|
const languageExtension = CODEMIRROR_LANGUAGES[lang] ? CODEMIRROR_LANGUAGES[lang]() : xml();
|
||||||
const cleanText = cleanHTML({ initialText });
|
const cleanText = cleanHTML({ initialText });
|
||||||
const newState = EditorState.create({
|
const newState = EditorState.create({
|
||||||
doc: cleanText,
|
doc: cleanText,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ exports[`SourceCodeModal renders as expected with default behavior 1`] = `
|
|||||||
>
|
>
|
||||||
<injectIntl(ShimmedIntlComponent)
|
<injectIntl(ShimmedIntlComponent)
|
||||||
innerRef="moCKrEf"
|
innerRef="moCKrEf"
|
||||||
|
lang="html"
|
||||||
value="mOckHtMl"
|
value="mOckHtMl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +40,7 @@ const SourceCodeModal = ({
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
innerRef={ref}
|
innerRef={ref}
|
||||||
value={value}
|
value={value}
|
||||||
|
lang="html" // hardcoded value to do lookup on CODEMIRROR_LANGUAGES
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ export const editorConfig = ({
|
|||||||
updateContent,
|
updateContent,
|
||||||
content,
|
content,
|
||||||
minHeight,
|
minHeight,
|
||||||
|
maxHeight,
|
||||||
learningContextId,
|
learningContextId,
|
||||||
staticRootUrl,
|
staticRootUrl,
|
||||||
enableImageUpload,
|
enableImageUpload,
|
||||||
@@ -335,6 +336,7 @@ export const editorConfig = ({
|
|||||||
content_css: false,
|
content_css: false,
|
||||||
content_style: tinyMCEStyles + a11ycheckerCss,
|
content_style: tinyMCEStyles + a11ycheckerCss,
|
||||||
min_height: minHeight,
|
min_height: minHeight,
|
||||||
|
max_height: maxHeight,
|
||||||
contextmenu: 'link table',
|
contextmenu: 'link table',
|
||||||
directionality: isLocaleRtl ? 'rtl' : 'ltr',
|
directionality: isLocaleRtl ? 'rtl' : 'ltr',
|
||||||
document_base_url: baseURL,
|
document_base_url: baseURL,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { getFileSizeToClosestByte } from '../../utils';
|
|||||||
import FileThumbnail from './FileThumbnail';
|
import FileThumbnail from './FileThumbnail';
|
||||||
import FileInfoModalSidebar from './FileInfoModalSidebar';
|
import FileInfoModalSidebar from './FileInfoModalSidebar';
|
||||||
import FileValidationModal from './FileValidationModal';
|
import FileValidationModal from './FileValidationModal';
|
||||||
|
import './FilesPage.scss';
|
||||||
|
|
||||||
const FilesPage = ({
|
const FilesPage = ({
|
||||||
courseId,
|
courseId,
|
||||||
|
|||||||
5
src/files-and-videos/files-page/FilesPage.scss
Normal file
5
src/files-and-videos/files-page/FilesPage.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.files-table {
|
||||||
|
.pgn__data-table-container {
|
||||||
|
overflow-x: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,15 +70,6 @@ const mockStore = async (
|
|||||||
}
|
}
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await executeThunk(fetchAssets(courseId), store.dispatch);
|
await executeThunk(fetchAssets(courseId), store.dispatch);
|
||||||
|
|
||||||
// Finish loading the expected files into the data table before returning,
|
|
||||||
// because loading new files can disrupt things like accessing file menus.
|
|
||||||
if (status === RequestStatus.SUCCESSFUL) {
|
|
||||||
const numFiles = skipNextPageFetch ? 13 : 15;
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(`Showing ${numFiles} of ${numFiles}`)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyMockStore = async (status) => {
|
const emptyMockStore = async (status) => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const slice = createSlice({
|
|||||||
if (isEmpty(state.assetIds)) {
|
if (isEmpty(state.assetIds)) {
|
||||||
state.assetIds = payload.assetIds;
|
state.assetIds = payload.assetIds;
|
||||||
} else {
|
} else {
|
||||||
state.assetIds = [...state.assetIds, ...payload.assetIds];
|
state.assetIds = [...new Set([...state.assetIds, ...payload.assetIds])];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setSortedAssetIds: (state, { payload }) => {
|
setSortedAssetIds: (state, { payload }) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AlertModal,
|
AlertModal,
|
||||||
Button,
|
Button,
|
||||||
Collapsible,
|
Collapsible,
|
||||||
|
DataTableContext,
|
||||||
Hyperlink,
|
Hyperlink,
|
||||||
Truncate,
|
Truncate,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
@@ -22,6 +23,13 @@ const DeleteConfirmationModal = ({
|
|||||||
// injected
|
// injected
|
||||||
intl,
|
intl,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { clearSelection } = useContext(DataTableContext);
|
||||||
|
|
||||||
|
const handleConfirmDeletion = () => {
|
||||||
|
handleBulkDelete();
|
||||||
|
clearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
const firstSelectedRow = selectedRows[0]?.original;
|
const firstSelectedRow = selectedRows[0]?.original;
|
||||||
let activeContentRows = [];
|
let activeContentRows = [];
|
||||||
if (Array.isArray(selectedRows)) {
|
if (Array.isArray(selectedRows)) {
|
||||||
@@ -73,7 +81,7 @@ const DeleteConfirmationModal = ({
|
|||||||
<Button variant="tertiary" onClick={closeDeleteConfirmation}>
|
<Button variant="tertiary" onClick={closeDeleteConfirmation}>
|
||||||
{intl.formatMessage(messages.cancelButtonLabel)}
|
{intl.formatMessage(messages.cancelButtonLabel)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleBulkDelete}>
|
<Button onClick={handleConfirmDeletion}>
|
||||||
{intl.formatMessage(messages.deleteFileButtonLabel)}
|
{intl.formatMessage(messages.deleteFileButtonLabel)}
|
||||||
</Button>
|
</Button>
|
||||||
</ActionRow>
|
</ActionRow>
|
||||||
|
|||||||
@@ -273,6 +273,16 @@ const FileTable = ({
|
|||||||
setSelectedRows={setSelectedRows}
|
setSelectedRows={setSelectedRows}
|
||||||
fileType={fileType}
|
fileType={fileType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteConfirmationModal
|
||||||
|
{...{
|
||||||
|
isDeleteConfirmationOpen,
|
||||||
|
closeDeleteConfirmation,
|
||||||
|
handleBulkDelete,
|
||||||
|
selectedRows,
|
||||||
|
fileType,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
<FileInput key="generic-file-upload" fileInput={fileInputControl} supportedFileFormats={supportedFileFormats} />
|
<FileInput key="generic-file-upload" fileInput={fileInputControl} supportedFileFormats={supportedFileFormats} />
|
||||||
{!isEmpty(selectedRows) && (
|
{!isEmpty(selectedRows) && (
|
||||||
@@ -286,15 +296,7 @@ const FileTable = ({
|
|||||||
sidebar={infoModalSidebar}
|
sidebar={infoModalSidebar}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DeleteConfirmationModal
|
|
||||||
{...{
|
|
||||||
isDeleteConfirmationOpen,
|
|
||||||
closeDeleteConfirmation,
|
|
||||||
handleBulkDelete,
|
|
||||||
selectedRows,
|
|
||||||
fileType,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,13 +26,18 @@ const TableActions = ({
|
|||||||
intl,
|
intl,
|
||||||
}) => {
|
}) => {
|
||||||
const [isSortOpen, openSort, closeSort] = useToggle(false);
|
const [isSortOpen, openSort, closeSort] = useToggle(false);
|
||||||
const { state } = useContext(DataTableContext);
|
const { state, clearSelection } = useContext(DataTableContext);
|
||||||
|
|
||||||
// This useEffect saves DataTable state so it can persist after table re-renders due to data reload.
|
// This useEffect saves DataTable state so it can persist after table re-renders due to data reload.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
|
const handleOpenFileSelector = () => {
|
||||||
|
fileInputControl.click();
|
||||||
|
clearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline-primary" onClick={openSort} iconBefore={Tune}>
|
<Button variant="outline-primary" onClick={openSort} iconBefore={Tune}>
|
||||||
@@ -71,7 +76,7 @@ const TableActions = ({
|
|||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button iconBefore={Add} onClick={fileInputControl.click}>
|
<Button iconBefore={Add} onClick={handleOpenFileSelector}>
|
||||||
{intl.formatMessage(messages.addFilesButtonLabel, { fileType })}
|
{intl.formatMessage(messages.addFilesButtonLabel, { fileType })}
|
||||||
</Button>
|
</Button>
|
||||||
<SortAndFilterModal {...{ isSortOpen, closeSort, handleSort }} />
|
<SortAndFilterModal {...{ isSortOpen, closeSort, handleSort }} />
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { DataTableContext } from '@openedx/paragon';
|
||||||
|
import { initializeMocks, render } from '../../../testUtils';
|
||||||
|
import TableActions from './TableActions';
|
||||||
|
import messages from '../messages';
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
selectedFlatRows: [],
|
||||||
|
fileInputControl: { click: jest.fn() },
|
||||||
|
handleOpenDeleteConfirmation: jest.fn(),
|
||||||
|
handleBulkDownload: jest.fn(),
|
||||||
|
encodingsDownloadUrl: null,
|
||||||
|
handleSort: jest.fn(),
|
||||||
|
fileType: 'video',
|
||||||
|
setInitialState: jest.fn(),
|
||||||
|
intl: {
|
||||||
|
formatMessage: (msg, values) => msg.defaultMessage.replace('{fileType}', values?.fileType ?? ''),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockColumns = [
|
||||||
|
{
|
||||||
|
id: 'wrapperType',
|
||||||
|
Header: 'Type',
|
||||||
|
accessor: 'wrapperType',
|
||||||
|
filter: 'includes',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderWithContext = (props = {}, contextOverrides = {}) => {
|
||||||
|
const contextValue = {
|
||||||
|
state: {
|
||||||
|
selectedRowIds: {},
|
||||||
|
filters: [],
|
||||||
|
...contextOverrides.state,
|
||||||
|
},
|
||||||
|
clearSelection: jest.fn(),
|
||||||
|
gotoPage: jest.fn(),
|
||||||
|
setAllFilters: jest.fn(),
|
||||||
|
columns: mockColumns,
|
||||||
|
...contextOverrides,
|
||||||
|
};
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<DataTableContext.Provider value={contextValue}>
|
||||||
|
<TableActions {...defaultProps} {...props} />
|
||||||
|
</DataTableContext.Provider>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('TableActions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMocks();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders buttons and dropdown', () => {
|
||||||
|
renderWithContext();
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: messages.sortButtonLabel.defaultMessage })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: messages.addFilesButtonLabel.defaultMessage.replace('{fileType}', 'video') })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disables bulk and delete actions if no rows selected', () => {
|
||||||
|
renderWithContext();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
|
||||||
|
|
||||||
|
const downloadOption = screen.getByText(messages.downloadTitle.defaultMessage);
|
||||||
|
const deleteButton = screen.getByTestId('open-delete-confirmation-button');
|
||||||
|
|
||||||
|
expect(downloadOption).toHaveAttribute('aria-disabled', 'true');
|
||||||
|
expect(downloadOption).toHaveClass('disabled');
|
||||||
|
|
||||||
|
expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
|
||||||
|
expect(deleteButton).toHaveClass('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enables bulk and delete actions when rows are selected', () => {
|
||||||
|
renderWithContext({
|
||||||
|
selectedFlatRows: [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
|
||||||
|
expect(screen.getByText(messages.downloadTitle.defaultMessage)).not.toBeDisabled();
|
||||||
|
expect(screen.getByTestId('open-delete-confirmation-button')).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls file input click and clears selection when add button clicked', () => {
|
||||||
|
const mockClick = jest.fn();
|
||||||
|
const mockClear = jest.fn();
|
||||||
|
|
||||||
|
renderWithContext({ fileInputControl: { click: mockClick } }, {}, mockClear);
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: messages.addFilesButtonLabel.defaultMessage.replace('{fileType}', 'video') }));
|
||||||
|
expect(mockClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opens sort modal when sort button clicked', () => {
|
||||||
|
renderWithContext();
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: messages.sortButtonLabel.defaultMessage }));
|
||||||
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls handleBulkDownload when selected and clicked', () => {
|
||||||
|
const handleBulkDownload = jest.fn();
|
||||||
|
renderWithContext({
|
||||||
|
selectedFlatRows: [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }],
|
||||||
|
handleBulkDownload,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
|
||||||
|
fireEvent.click(screen.getByText(messages.downloadTitle.defaultMessage));
|
||||||
|
expect(handleBulkDownload).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls handleOpenDeleteConfirmation when clicked', () => {
|
||||||
|
const handleOpenDeleteConfirmation = jest.fn();
|
||||||
|
const selectedFlatRows = [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }];
|
||||||
|
renderWithContext({
|
||||||
|
selectedFlatRows,
|
||||||
|
handleOpenDeleteConfirmation,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
|
||||||
|
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
|
||||||
|
expect(handleOpenDeleteConfirmation).toHaveBeenCalledWith(selectedFlatRows);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows encoding download link when provided', () => {
|
||||||
|
const encodingsDownloadUrl = '/some/path/to/encoding.zip';
|
||||||
|
renderWithContext({ encodingsDownloadUrl });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage }));
|
||||||
|
expect(screen.getByRole('link', { name: messages.downloadEncodingsTitle.defaultMessage })).toHaveAttribute('href', expect.stringContaining(encodingsDownloadUrl));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -48,21 +48,30 @@ const VideoThumbnail = ({
|
|||||||
const isFailed = VIDEO_FAILURE_STATUSES.includes(status);
|
const isFailed = VIDEO_FAILURE_STATUSES.includes(status);
|
||||||
const failedMessage = intl.formatMessage(messages.failedCheckboxLabel);
|
const failedMessage = intl.formatMessage(messages.failedCheckboxLabel);
|
||||||
|
|
||||||
const showThumbnail = allowThumbnailUpload && thumbnail && isUploaded;
|
const showThumbnail = allowThumbnailUpload && isUploaded;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="video-thumbnail row justify-content-center align-itmes-center">
|
<div className="video-thumbnail row justify-content-center align-itmes-center">
|
||||||
{allowThumbnailUpload && showThumbnail && <div className="thumbnail-overlay" />}
|
{allowThumbnailUpload && isUploaded && <div className="thumbnail-overlay" />}
|
||||||
{showThumbnail && !thumbnailError && pageLoadStatus === RequestStatus.SUCCESSFUL ? (
|
{showThumbnail && !thumbnailError && pageLoadStatus === RequestStatus.SUCCESSFUL ? (
|
||||||
<>
|
<>
|
||||||
<div className="border rounded">
|
<div className="border rounded">
|
||||||
<Image
|
{ thumbnail ? (
|
||||||
style={imageSize}
|
<Image
|
||||||
className="m-1 bg-light-300"
|
style={imageSize}
|
||||||
src={thumbnail}
|
className="m-1 bg-light-300"
|
||||||
alt={intl.formatMessage(messages.thumbnailAltMessage, { displayName })}
|
src={thumbnail}
|
||||||
onError={() => setThumbnailError(true)}
|
alt={intl.formatMessage(messages.thumbnailAltMessage, { displayName })}
|
||||||
/>
|
onError={() => setThumbnailError(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="row justify-content-center align-items-center m-0"
|
||||||
|
style={imageSize}
|
||||||
|
>
|
||||||
|
<Icon src={VideoFile} style={{ height: '48px', width: '48px' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="add-thumbnail" data-testid={`video-thumbnail-${id}`}>
|
<div className="add-thumbnail" data-testid={`video-thumbnail-${id}`}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
32
src/files-and-videos/videos-page/VideoThumbnail.test.jsx
Normal file
32
src/files-and-videos/videos-page/VideoThumbnail.test.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import VideoThumbnail from './VideoThumbnail';
|
||||||
|
import { VIDEO_SUCCESS_STATUSES } from './data/constants';
|
||||||
|
import { RequestStatus } from '../../data/constants';
|
||||||
|
|
||||||
|
it('shows fallback icon if thumbnail fails to load', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<VideoThumbnail
|
||||||
|
thumbnail="http://bad-url/image.png"
|
||||||
|
displayName="Test Video"
|
||||||
|
id="vid1"
|
||||||
|
imageSize={{ width: '100px', height: '100px' }}
|
||||||
|
handleAddThumbnail={jest.fn()}
|
||||||
|
videoImageSettings={{ videoImageUploadEnabled: true, supportedFileFormats: { jpg: 'image/jpg' } }}
|
||||||
|
status={VIDEO_SUCCESS_STATUSES[0]}
|
||||||
|
pageLoadStatus={RequestStatus.SUCCESSFUL}
|
||||||
|
/>
|
||||||
|
</IntlProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = screen.getByRole('img', { name: /video thumbnail/i });
|
||||||
|
expect(image).toBeInTheDocument();
|
||||||
|
fireEvent.error(image);
|
||||||
|
|
||||||
|
expect(screen.queryByRole('img', { name: /video thumbnail/i })).toBeNull();
|
||||||
|
|
||||||
|
const fallbackSvg = container.querySelector('svg[role="img"]');
|
||||||
|
expect(fallbackSvg).toBeInTheDocument();
|
||||||
|
});
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import React, { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
PointerSensor,
|
PointerSensor,
|
||||||
useSensor,
|
useSensor,
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||||
|
import { verticalSortableListCollisionDetection } from './verticalSortableList';
|
||||||
|
|
||||||
const DraggableList = ({
|
const DraggableList = ({
|
||||||
itemList,
|
itemList,
|
||||||
@@ -56,13 +56,20 @@ const DraggableList = ({
|
|||||||
setActiveId?.(event.active.id);
|
setActiveId?.(event.active.id);
|
||||||
}, [setActiveId]);
|
}, [setActiveId]);
|
||||||
|
|
||||||
|
const handleDragCancel = useCallback(() => {
|
||||||
|
setActiveId?.(null);
|
||||||
|
}, [setActiveId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
modifiers={[restrictToVerticalAxis]}
|
modifiers={[restrictToVerticalAxis]}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={verticalSortableListCollisionDetection}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
|
// autoScroll does not play well with verticalSortableListCollisionDetection strategy
|
||||||
|
autoScroll={false}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={itemList}
|
items={itemList}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const SortableItem = ({
|
|||||||
isClickable,
|
isClickable,
|
||||||
onClick,
|
onClick,
|
||||||
disabled,
|
disabled,
|
||||||
|
cardClassName = '',
|
||||||
// injected
|
// injected
|
||||||
intl,
|
intl,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -45,14 +46,15 @@ const SortableItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
style={style}
|
style={style}
|
||||||
className="mx-0"
|
className={`mx-0 ${cardClassName}`}
|
||||||
isClickable={isClickable}
|
isClickable={isClickable}
|
||||||
onClick={onClick}
|
|
||||||
>
|
>
|
||||||
<ActionRow style={actionStyle}>
|
<ActionRow style={actionStyle}>
|
||||||
{actions}
|
{actions}
|
||||||
@@ -93,6 +95,7 @@ SortableItem.propTypes = {
|
|||||||
isClickable: PropTypes.bool,
|
isClickable: PropTypes.bool,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
cardClassName: PropTypes.string,
|
||||||
// injected
|
// injected
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
80
src/generic/DraggableList/verticalSortableList.ts
Normal file
80
src/generic/DraggableList/verticalSortableList.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/**
|
||||||
|
This sorting strategy was copied over from https://github.com/clauderic/dnd-kit/pull/805
|
||||||
|
to resolve issues with variable sized draggables.
|
||||||
|
*/
|
||||||
|
import { CollisionDetection, DroppableContainer } from '@dnd-kit/core';
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
|
|
||||||
|
const collision = (dropppableContainer?: DroppableContainer) => ({
|
||||||
|
id: dropppableContainer?.id ?? '',
|
||||||
|
value: dropppableContainer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Look for the first (/ furthest up / highest) droppable container that is at least
|
||||||
|
// 50% covered by the top edge of the dragging container.
|
||||||
|
const highestDroppableContainerMajorityCovered: CollisionDetection = ({
|
||||||
|
droppableContainers,
|
||||||
|
collisionRect,
|
||||||
|
}) => {
|
||||||
|
const ascendingDroppabaleContainers = sortBy(
|
||||||
|
droppableContainers,
|
||||||
|
(c) => c?.rect.current?.top,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const droppableContainer of ascendingDroppabaleContainers) {
|
||||||
|
const {
|
||||||
|
rect: { current: droppableRect },
|
||||||
|
} = droppableContainer;
|
||||||
|
|
||||||
|
if (droppableRect) {
|
||||||
|
const coveredPercentage = (droppableRect.top + droppableRect.height - collisionRect.top)
|
||||||
|
/ droppableRect.height;
|
||||||
|
|
||||||
|
if (coveredPercentage > 0.5) {
|
||||||
|
return [collision(droppableContainer)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we haven't found anything then we are off the top, so return the first item
|
||||||
|
return [collision(ascendingDroppabaleContainers[0])];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look for the last (/ furthest down / lowest) droppable container that is at least
|
||||||
|
// 50% covered by the bottom edge of the dragging container.
|
||||||
|
const lowestDroppableContainerMajorityCovered: CollisionDetection = ({
|
||||||
|
droppableContainers,
|
||||||
|
collisionRect,
|
||||||
|
}) => {
|
||||||
|
const descendingDroppabaleContainers = sortBy(
|
||||||
|
droppableContainers,
|
||||||
|
(c) => c?.rect.current?.top,
|
||||||
|
).reverse();
|
||||||
|
|
||||||
|
for (const droppableContainer of descendingDroppabaleContainers) {
|
||||||
|
const {
|
||||||
|
rect: { current: droppableRect },
|
||||||
|
} = droppableContainer;
|
||||||
|
|
||||||
|
if (droppableRect) {
|
||||||
|
const coveredPercentage = (collisionRect.bottom - droppableRect.top) / droppableRect.height;
|
||||||
|
|
||||||
|
if (coveredPercentage > 0.5) {
|
||||||
|
return [collision(droppableContainer)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we haven't found anything then we are off the bottom, so return the last item
|
||||||
|
return [collision(descendingDroppabaleContainers[0])];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verticalSortableListCollisionDetection: CollisionDetection = (
|
||||||
|
args,
|
||||||
|
) => {
|
||||||
|
if (args.collisionRect.top < (args.active.rect.current?.initial?.top ?? 0)) {
|
||||||
|
return highestDroppableContainerMajorityCovered(args);
|
||||||
|
}
|
||||||
|
return lowestDroppableContainerMajorityCovered(args);
|
||||||
|
};
|
||||||
6
src/generic/alert-message/index.scss
Normal file
6
src/generic/alert-message/index.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// TODO: remove this after upstream fix merging: https://github.com/openedx/paragon/pull/3562
|
||||||
|
.alert {
|
||||||
|
.alert-message-content {
|
||||||
|
align-self: baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
ActionRow,
|
ActionRow,
|
||||||
Button,
|
Button,
|
||||||
@@ -9,17 +8,29 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import LoadingButton from '../loading-button';
|
import LoadingButton from '../loading-button';
|
||||||
|
|
||||||
|
interface DeleteModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
close: () => void;
|
||||||
|
category?: string;
|
||||||
|
onDeleteSubmit: () => void | Promise<void>;
|
||||||
|
title?: string;
|
||||||
|
description?: React.ReactNode | React.ReactNode[];
|
||||||
|
variant?: string;
|
||||||
|
btnLabel?: string;
|
||||||
|
icon?: React.ElementType;
|
||||||
|
}
|
||||||
|
|
||||||
const DeleteModal = ({
|
const DeleteModal = ({
|
||||||
category,
|
category = '',
|
||||||
isOpen,
|
isOpen,
|
||||||
close,
|
close,
|
||||||
onDeleteSubmit,
|
onDeleteSubmit,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
variant,
|
variant = 'default',
|
||||||
btnLabel,
|
btnLabel,
|
||||||
icon,
|
icon,
|
||||||
}) => {
|
}: DeleteModalProps) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const modalTitle = title || intl.formatMessage(messages.title, { category });
|
const modalTitle = title || intl.formatMessage(messages.title, { category });
|
||||||
@@ -62,28 +73,4 @@ const DeleteModal = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DeleteModal.defaultProps = {
|
|
||||||
category: '',
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
variant: 'default',
|
|
||||||
btnLabel: '',
|
|
||||||
icon: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
DeleteModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
close: PropTypes.func.isRequired,
|
|
||||||
category: PropTypes.string,
|
|
||||||
onDeleteSubmit: PropTypes.func.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.oneOfType([
|
|
||||||
PropTypes.element,
|
|
||||||
PropTypes.string,
|
|
||||||
]),
|
|
||||||
variant: PropTypes.string,
|
|
||||||
btnLabel: PropTypes.string,
|
|
||||||
icon: PropTypes.elementType,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeleteModal;
|
export default DeleteModal;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user