Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: block creation in libraries v2 #1574

Merged
merged 21 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7fd9c16
refactor: block creation for text component in librries v2
dcoa Dec 17, 2024
f465101
refactor: allow init the modal for video creation
dcoa Dec 17, 2024
e46bc9d
refactor: allow open the problem modal when is a new component
dcoa Dec 17, 2024
287c67c
fix: clean the editor store when the moda is close
dcoa Dec 18, 2024
a8c61a9
test: add testing for request
dcoa Dec 27, 2024
1038774
test: testing for editor container hook
dcoa Dec 31, 2024
9006ad7
test: refactor editor testing
dcoa Jan 14, 2025
71f04f7
feat: implement fist versioning of image upload for creation workflow
dcoa Jan 16, 2025
378dacf
feat: allow to update images in the creation workflow
dcoa Jan 17, 2025
28ce705
fix: create problem with unique id
dcoa Jan 20, 2025
896851e
test: refactor add content library test
dcoa Jan 21, 2025
b462301
feat: add error message in editor create block
dcoa Jan 23, 2025
9723c7b
test: update testing
dcoa Jan 24, 2025
86e2b22
refactor: change isCreateBlock for shouldCreateBlock
dcoa Feb 10, 2025
f44ce1e
fix: keep collection linkComponent function when the modal is close
dcoa Jan 29, 2025
a43c76c
test: increse coverage for EditorContainer component
dcoa Jan 30, 2025
92e12e9
refactor: modify create unique block key
dcoa Feb 3, 2025
d3ee603
fix: validate data for onClose function
dcoa Feb 10, 2025
6709576
refactor: replace hash for suffix to create the unique id
dcoa Feb 12, 2025
77a0805
test: create a removeTemporalLink to increase test coverage
dcoa Feb 24, 2025
f4490ca
fix: linter and testing mock block title
dcoa Feb 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ INVITE_STUDENTS_EMAIL_TO="[email protected]"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2"
# "other" is used to test the workflow for creating blocks that aren't supported by the built-in editors
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2,other"
29 changes: 29 additions & 0 deletions src/editors/containers/EditorContainer/hooks.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jest.mock('../../data/redux', () => ({
app: {
isInitialized: (state) => ({ isInitialized: state }),
images: (state) => ({ images: state }),
shouldCreateBlock: (state) => ({ shouldCreateBlock: state }),
},
requests: {
isFailed: (...args) => ({ requestFailed: args }),
Expand All @@ -26,6 +27,7 @@ jest.mock('../../hooks', () => ({
...jest.requireActual('../../hooks'),
navigateCallback: jest.fn((args) => ({ navigateCallback: args })),
saveBlock: jest.fn((args) => ({ saveBlock: args })),
createBlock: jest.fn((args) => ({ createBlock: args })),
}));

const dispatch = jest.fn();
Expand Down Expand Up @@ -53,6 +55,8 @@ describe('EditorContainer hooks', () => {
const getContent = () => 'myTestContentValue';
const setAssetToStaticUrl = () => 'myTestContentValue';
const validateEntry = () => 'vaLIdAteENTry';
reactRedux.useSelector.mockReturnValue(false);

const output = hooks.handleSaveClicked({
getContent,
images: {
Expand All @@ -73,6 +77,31 @@ describe('EditorContainer hooks', () => {
validateEntry,
});
});
it('returns callback to createBlock with dispatch and content if shouldCreateBlock is true', () => {
const getContent = () => 'myTestContentValue';
const setAssetToStaticUrl = () => 'myTestContentValue';
const validateEntry = () => 'vaLIdAteENTry';
reactRedux.useSelector.mockReturnValue(true);

const output = hooks.handleSaveClicked({
getContent,
images: {
portableUrl: '/static/sOmEuiMAge.jpeg',
displayName: 'sOmEuiMAge',
},
destination: 'testDEsTURL',
analytics: 'soMEanALytics',
dispatch,
validateEntry,
});
output();
expect(appHooks.createBlock).toHaveBeenCalledWith({
content: setAssetToStaticUrl(reactRedux.useSelector(selectors.app.images), getContent),
destination: reactRedux.useSelector(selectors.app.returnUrl),
analytics: reactRedux.useSelector(selectors.app.analytics),
dispatch,
});
});
});

describe('cancelConfirmModalToggle', () => {
Expand Down
25 changes: 24 additions & 1 deletion src/editors/containers/EditorContainer/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import * as appHooks from '../../hooks';

export const {
clearSaveError,
clearCreateError,
navigateCallback,
nullMethod,
saveBlock,
createBlock,
} = appHooks;

export const state = StrictDict({
Expand All @@ -27,10 +29,20 @@ export const handleSaveClicked = ({
}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const returnUrl = useSelector(selectors.app.returnUrl);
// eslint-disable-next-line react-hooks/rules-of-hooks
const createBlockOnSave = useSelector(selectors.app.shouldCreateBlock);
const destination = returnFunction ? '' : returnUrl;
// eslint-disable-next-line react-hooks/rules-of-hooks
const analytics = useSelector(selectors.app.analytics);

if (createBlockOnSave) {
return () => createBlock({
analytics,
content: getContent({ dispatch }),
destination,
dispatch,
returnFunction,
});
}
return () => saveBlock({
analytics,
content: getContent({ dispatch }),
Expand Down Expand Up @@ -79,3 +91,14 @@ export const isInitialized = () => useSelector(selectors.app.isInitialized);
export const saveFailed = () => useSelector((rootState) => (
selectors.requests.isFailed(rootState, { requestKey: RequestKeys.saveBlock })
));

export const createFailed = () => ({
// eslint-disable-next-line react-hooks/rules-of-hooks
createFailed: useSelector((rootState) => (
selectors.requests.isFailed(rootState, { requestKey: RequestKeys.createBlock })
)),
// eslint-disable-next-line react-hooks/rules-of-hooks
createFailedError: useSelector((rootState) => (
selectors.requests.error(rootState, { requestKey: RequestKeys.createBlock })
)),
});
33 changes: 22 additions & 11 deletions src/editors/containers/EditorContainer/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import editorCmsApi from '../../data/services/cms/api';

import EditorPage from '../../EditorPage';
import * as hooks from './hooks';

// Mock this plugins component:
jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCss: '' }));
Expand All @@ -17,17 +18,6 @@ jest.spyOn(editorCmsApi, 'fetchCourseImages').mockImplementation(async () => ( /
{ data: { assets: [], start: 0, end: 0, page: 0, pageSize: 50, totalCount: 0 } }
));
// Mock out the 'get ancestors' API:
jest.spyOn(editorCmsApi, 'fetchByUnitId').mockImplementation(async () => ({
status: 200,
data: {
ancestors: [{
id: 'block-v1:Org+TS100+24+type@vertical+block@parent',
display_name: 'You-Knit? The Test Unit',
category: 'vertical',
has_children: true,
}],
},
}));

const isDirtyMock = jest.fn();
jest.mock('../TextEditor/hooks', () => ({
Expand Down Expand Up @@ -60,6 +50,17 @@ describe('EditorContainer', () => {
jest.spyOn(window, 'removeEventListener');
jest.spyOn(mockEvent, 'preventDefault');
Object.defineProperty(mockEvent, 'returnValue', { writable: true });
jest.spyOn(editorCmsApi, 'fetchByUnitId').mockImplementation(async () => ({
status: 200,
data: {
ancestors: [{
id: 'block-v1:Org+TS100+24+type@vertical+block@parent',
display_name: 'You-Knit? The Test Unit',
category: 'vertical',
has_children: true,
}],
},
}));
});

afterEach(() => {
Expand Down Expand Up @@ -165,4 +166,14 @@ describe('EditorContainer', () => {
expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockEvent.returnValue).toBe(true);
});
test('should display an alert when is an error creating a new block', async () => {
jest.spyOn(hooks, 'createFailed').mockImplementation(() => ({ createFailed: true, createFailedError: 'error' }));
render(<EditorPage {...{ ...defaultPropsHtml, blockId: '' }} />);
expect(await screen.findByText(/There was an error creating the content/i)).toBeInTheDocument();
});
test('should display an alert when is an error saving the changes', async () => {
jest.spyOn(hooks, 'saveFailed').mockImplementation(() => true);
render(<EditorPage {...defaultPropsHtml} />);
expect(await screen.findByText(/Error: Content save failed. Please check recent changes and try again later./i)).toBeInTheDocument();
});
});
17 changes: 17 additions & 0 deletions src/editors/containers/EditorContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { useEditorContext } from '../../EditorContext';
import TitleHeader from './components/TitleHeader';
import * as hooks from './hooks';
import messages from './messages';
import { parseErrorMsg } from '../../../library-authoring/add-content/AddContentContainer';
import libraryMessages from '../../../library-authoring/add-content/messages';

import './index.scss';
import usePromptIfDirty from '../../../generic/promptIfDirty/usePromptIfDirty';
import CancelConfirmModal from './components/CancelConfirmModal';
Expand Down Expand Up @@ -81,9 +84,12 @@ const EditorContainer: React.FC<Props> = ({
const isInitialized = hooks.isInitialized();
const { isCancelConfirmOpen, openCancelConfirmModal, closeCancelConfirmModal } = hooks.cancelConfirmModalToggle();
const handleCancel = hooks.handleCancel({ onClose, returnFunction });
const { createFailed, createFailedError } = hooks.createFailed();
const disableSave = !isInitialized;
const saveFailed = hooks.saveFailed();
const clearSaveFailed = hooks.clearSaveError({ dispatch });
const clearCreateFailed = hooks.clearCreateError({ dispatch });

const handleSave = hooks.handleSaveClicked({
dispatch,
getContent,
Expand Down Expand Up @@ -113,6 +119,16 @@ const EditorContainer: React.FC<Props> = ({
};
return (
<EditorModalWrapper onClose={confirmCancelIfDirty}>
{createFailed && (
<Toast show onClose={clearCreateFailed}>
{parseErrorMsg(
intl,
createFailedError,
libraryMessages.errorCreateMessageWithDetail,
libraryMessages.errorCreateMessage,
)}
</Toast>
)}
{saveFailed && (
<Toast show onClose={clearSaveFailed}>
<FormattedMessage {...messages.contentSaveFailed} />
Expand All @@ -126,6 +142,7 @@ const EditorContainer: React.FC<Props> = ({
if (returnFunction) {
closeCancelConfirmModal();
}
dispatch({ type: 'resetEditor' });
}}
/>
<ModalDialog.Header className="shadow-sm zindex-10">
Expand Down
11 changes: 9 additions & 2 deletions src/editors/containers/ProblemEditor/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jest.mock('../../data/redux', () => ({
selectors: {
app: {
blockValue: jest.fn(state => ({ blockValue: state })),
shouldCreateBlock: jest.fn(state => ({ shouldCreateBlock: state })),
},
problem: {
problemType: jest.fn(state => ({ problemType: state })),
Expand Down Expand Up @@ -104,12 +105,18 @@ describe('ProblemEditor', () => {
test('blockFinished from requests.isFinished', () => {
expect(
mapStateToProps(testState).blockFinished,
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock }));
).toEqual(
selectors.app.shouldCreateBlock(testState)
|| selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock }),
);
});
test('advancedSettingsFinished from requests.isFinished', () => {
expect(
mapStateToProps(testState).advancedSettingsFinished,
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAdvancedSettings }));
).toEqual(
selectors.app.shouldCreateBlock(testState)
|| selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAdvancedSettings }),
);
});
});
describe('mapDispatchToProps', () => {
Expand Down
6 changes: 4 additions & 2 deletions src/editors/containers/ProblemEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ const ProblemEditor: React.FC<Props> = ({
};

export const mapStateToProps = (state) => ({
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
blockFinished: selectors.app.shouldCreateBlock(state)
|| selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }),
problemType: selectors.problem.problemType(state),
blockValue: selectors.app.blockValue(state),
advancedSettingsFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAdvancedSettings }),
advancedSettingsFinished: selectors.app.shouldCreateBlock(state)
|| selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAdvancedSettings }),
});

export const mapDispatchToProps = {
Expand Down
3 changes: 2 additions & 1 deletion src/editors/containers/TextEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ export const mapStateToProps = (state) => ({
blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }),
blockId: selectors.app.blockId(state),
showRawEditor: selectors.app.showRawEditor(state),
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
blockFinished: selectors.app.shouldCreateBlock(state)
|| selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
learningContextId: selectors.app.learningContextId(state),
images: selectors.app.images(state),
isLibrary: selectors.app.isLibrary(state),
Expand Down
4 changes: 3 additions & 1 deletion src/editors/containers/TextEditor/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jest.mock('../../data/redux', () => ({
selectors: {
app: {
blockValue: jest.fn(state => ({ blockValue: state })),
shouldCreateBlock: jest.fn(state => ({ shouldCreateBlock: state })),
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
showRawEditor: jest.fn(state => ({ showRawEditor: state })),
Expand Down Expand Up @@ -126,7 +127,8 @@ describe('TextEditor', () => {
test('blockFinished from requests.isFinished', () => {
expect(
mapStateToProps(testState).blockFinished,
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock }));
).toEqual(selectors.app.shouldCreateBlock(testState)
|| selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock }));
});
test('learningContextId from app.learningContextId', () => {
expect(
Expand Down
3 changes: 2 additions & 1 deletion src/editors/containers/VideoEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const VideoEditor: React.FC<EditorComponent> = ({
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchStudioView }),
);
const isLibrary = useSelector(selectors.app.isLibrary) as boolean;
const isCreateWorkflow = useSelector(selectors.app.shouldCreateBlock) as boolean;
const {
error,
validateEntry,
Expand All @@ -36,7 +37,7 @@ const VideoEditor: React.FC<EditorComponent> = ({
returnFunction={returnFunction}
validateEntry={validateEntry}
>
{studioViewFinished ? (
{(isCreateWorkflow || studioViewFinished) ? (
<div className="video-editor">
<VideoEditorModal {...{ isLibrary }} />
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/editors/data/constants/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,7 @@ export const ignoredOlxAttributes = [
'@_url_name',
'@_x-is-pointer-node',
] as const;

// Useful for the block creation workflow.
export const problemTitles = new Set([...Object.values(ProblemTypes).map((problem) => problem.title),
...Object.values(AdvanceProblems).map((problem) => problem.title)]);
2 changes: 2 additions & 0 deletions src/editors/data/constants/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const RequestKeys = StrictDict({
fetchImages: 'fetchImages',
fetchUnit: 'fetchUnit',
fetchStudioView: 'fetchStudioView',
createBlock: 'createBlock',
saveBlock: 'saveBlock',
uploadVideo: 'uploadVideo',
allowThumbnailUpload: 'allowThumbnailUpload',
Expand All @@ -25,6 +26,7 @@ export const RequestKeys = StrictDict({
checkTranscriptsForImport: 'checkTranscriptsForImport',
importTranscript: 'importTranscript',
uploadAsset: 'uploadAsset',
batchUploadAssets: 'batchUploadAssets',
fetchAdvancedSettings: 'fetchAdvancedSettings',
fetchVideoFeatures: 'fetchVideoFeatures',
getHandlerUrl: 'getHandlerUrl',
Expand Down
1 change: 0 additions & 1 deletion src/editors/data/redux/app/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const app = createSlice({
images: { ...state.images, ...payload.images },
imageCount: payload.imageCount,
}),
resetImages: (state) => ({ ...state, images: {}, imageCount: 0 }),
setVideos: (state, { payload }) => ({ ...state, videos: payload }),
setCourseDetails: (state, { payload }) => ({ ...state, courseDetails: payload }),
setShowRawEditor: (state, { payload }) => ({
Expand Down
26 changes: 21 additions & 5 deletions src/editors/data/redux/app/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe('app selectors unit tests', () => {
simpleSelectors.unitUrl,
simpleSelectors.blockValue,
selectors.isLibrary,
selectors.shouldCreateBlock,
]);
});
describe('for library blocks', () => {
Expand All @@ -98,8 +99,8 @@ describe('app selectors unit tests', () => {
};

[
[[null, truthy.blockValue, true] as [any, any, any], true] as const,
[[null, null, true] as [any, any, any], false] as const,
[[null, truthy.blockValue, true, false] as [any, any, any, any], true] as const,
[[null, null, true, false] as [any, any, any, any], false] as const,
].map(([args, expected]) => expect(cb(...args)).toEqual(expected));
});
});
Expand All @@ -112,9 +113,19 @@ describe('app selectors unit tests', () => {
};

[
[[null, truthy.blockValue, false] as [any, any, any], false] as const,
[[truthy.unitUrl, null, false] as [any, any, any], false] as const,
[[truthy.unitUrl, truthy.blockValue, false] as [any, any, any], true] as const,
[[null, truthy.blockValue, false, false] as [any, any, any, any], false] as const,
[[truthy.unitUrl, null, false, false] as [any, any, any, any], false] as const,
[[truthy.unitUrl, truthy.blockValue, false, false] as [any, any, any, any], true] as const,
].map(([args, expected]) => expect(cb(...args)).toEqual(expected));
});
});
describe('component creation workflow', () => {
it('returns true if is shouldCreateBlock is truthy', () => {
const { resultFunc: cb } = selectors.isInitialized;

[
[[null, null, true, true] as [any, any, any, any], true] as const,
[[null, null, true, true] as [any, any, any, any], true] as const,
].map(([args, expected]) => expect(cb(...args)).toEqual(expected));
});
});
Expand Down Expand Up @@ -184,4 +195,9 @@ describe('app selectors unit tests', () => {
});
});
});
describe('shouldCreateBlock', () => {
it('should return false if the editor is initialized with a blockId', () => {
expect(selectors.shouldCreateBlock.resultFunc('block-v1:', 'text')).toEqual(false);
});
});
});
Loading