From 27b2c6f78d9e4ed7107f7e166a2ceb6c4cde2c89 Mon Sep 17 00:00:00 2001 From: connorhaugh Date: Tue, 17 Oct 2023 20:08:48 +0000 Subject: [PATCH 01/46] feat: initial editor layout + ui --- .../BlockSettingsEditor.jsx | 88 ++++++++++++++ .../library_contentEditor/LibrarySelector.jsx | 35 ++++++ .../library_contentEditor/constants.js | 16 +++ .../library_contentEditor/data/api.js | 0 .../library_contentEditor/data/reducers.js | 51 ++++++++ .../library_contentEditor/data/selectors.js | 20 ++++ .../library_contentEditor/data/urls.js | 0 .../library_contentEditor/index.jsx | 111 ++++++++++++++++++ .../library_contentEditor/messages.js | 22 ++++ src/editors/data/constants/app.js | 2 + src/editors/supportedEditors.js | 4 + 11 files changed, 349 insertions(+) create mode 100644 src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx create mode 100644 src/editors/containers/library_contentEditor/LibrarySelector.jsx create mode 100644 src/editors/containers/library_contentEditor/constants.js create mode 100644 src/editors/containers/library_contentEditor/data/api.js create mode 100644 src/editors/containers/library_contentEditor/data/reducers.js create mode 100644 src/editors/containers/library_contentEditor/data/selectors.js create mode 100644 src/editors/containers/library_contentEditor/data/urls.js create mode 100644 src/editors/containers/library_contentEditor/index.jsx create mode 100644 src/editors/containers/library_contentEditor/messages.js diff --git a/src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx b/src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx new file mode 100644 index 000000000..987ad9d6c --- /dev/null +++ b/src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { modes } from './constants'; +import { selectors, thunkActions } from '../../data/redux'; + +const BlockSettingsEditor = ({ + selectionMode, + onSelectionModeChange, + selectionSettings, + blocksInSelectedLibrary, + onSelectionSettingsChange, + }) => { + + const getSelectionSettings = () =>{ + if (selectionMode === modes.all){ + return (<>) + } + if (selectionMode === modes.random){ + return ( + <> + + onSelectionSettingsChange({ + count: e.target.value, + ...selectionSettings + })} + floatingLabel="How many blocks do you want to show the author?" + /> + + + onSelectionSettingsChange({ + showReset: e.target.checked, + ...selectionSettings + })} + > + Show Reset Button + +
+ Determines whether a 'Reset Problems' button is shown, so users may reset their answers and reshuffle selected items. +
+
+ {/* TODO: ADD CAPA FILTERING FOR V1 ONLY */} + + ) + } + if (selectionMode === modes.selected){ + return ( + <> + {/*TODO: ADD BLOCK PICKER*/} +

Block Selection Can be Made by Saving the editor and clicking the "view" button or going here.

+ {/* Opens library page in new window.*/} + https://studio.edx.org/container/block-v1:edX+LA101+2022_Summer+type@library_content+block@6a0e7d3c67614ae78e28d575408624cf + + ) + } + } + return ( +
+ + + { + modes.values().map(mode => ()) + } + + {getSelectionSettings()} + +
+ ); +}; + +export const mapStateToProps = (state) => ({ + selectionMode: selectors.library.selectionMode(state), + selectionSettings: selectors.library.selectionSettings(state), + blocksInSelectedLibrary: selectors.library.blocksInSelectedLibrary(state), +}) + +export const mapDispatchToProps = { + onSelectionModeChange: thunkActions.library.onSelectionModeChange, + onSelectionSettingsChange: thunkActions.library.onSelectionSettingsChange, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibraryBlockPicker)); \ No newline at end of file diff --git a/src/editors/containers/library_contentEditor/LibrarySelector.jsx b/src/editors/containers/library_contentEditor/LibrarySelector.jsx new file mode 100644 index 000000000..e22f91510 --- /dev/null +++ b/src/editors/containers/library_contentEditor/LibrarySelector.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Dropdown } from '@edx/paragon'; + + +const LibrarySelector = ({ libraries, selectedLibrary, onSelectLibrary }) => { + return ( +
+ + ({ + label: library.name, + value: library.id, + }))} + value={selectedLibrary} + onChange={onSelectLibrary} + /> +
+ ); +}; + +export const mapStateToProps = (state) => ({ + selectionMode: selectors.library.libraries(state), + selectionSettings: selectors.library.selectedLibrary(state), +}) + +export const mapDispatchToProps = { + onSelectLibrary: reducers.library.onSelectLibrary, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibrarySelector)); + + diff --git a/src/editors/containers/library_contentEditor/constants.js b/src/editors/containers/library_contentEditor/constants.js new file mode 100644 index 000000000..ee23162e8 --- /dev/null +++ b/src/editors/containers/library_contentEditor/constants.js @@ -0,0 +1,16 @@ +import messages from "./messages" + +export const modes = { + all: { + description: messages.modeAll, + value: 'all' + }, + random: { + description: messages.modeRandom, + value: 'random' + }, + selected: { + description: messages.modeSelected, + value: 'selected' + } +} diff --git a/src/editors/containers/library_contentEditor/data/api.js b/src/editors/containers/library_contentEditor/data/api.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/editors/containers/library_contentEditor/data/reducers.js b/src/editors/containers/library_contentEditor/data/reducers.js new file mode 100644 index 000000000..3e4155f19 --- /dev/null +++ b/src/editors/containers/library_contentEditor/data/reducers.js @@ -0,0 +1,51 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { modes } from '../constants'; + +const initialState = { + libraries: [], + selectedLibrary: null, + selectionMode: modes.all, + selectionSettings: { + count: false, + showReset: false, + }, + blocksInSelectedLibrary: [], +}; + +const library = createSlice({ + name: 'library', + initialState, + reducers: { + initialize: (state, { payload }) => ({ + ...state, + studioEndpointUrl: payload.studioEndpointUrl, + lmsEndpointUrl: payload.lmsEndpointUrl, + blockId: payload.blockId, + learningContextId: payload.learningContextId, + blockType: payload.blockType, + blockValue: null, + }), + onSelectLibrary: (state, {payload}) => ({ + ...state, + selectedLibrary: payload.selectedLibrary, + }), + onSelectionModeChange: (state, {payload}) => ({ + ...state, + selectionMode: payload.selectionMode, + }), + onSelectionSettingsChange: (state,{payload})=>({ + ...state, + selectionSettings:payload.selectionSettings, + }), + }, + }); + +const actions = StrictDict(app.actions); + +const { reducer } = library; + +export { + actions, + initialState, + reducer, +}; \ No newline at end of file diff --git a/src/editors/containers/library_contentEditor/data/selectors.js b/src/editors/containers/library_contentEditor/data/selectors.js new file mode 100644 index 000000000..fb5fbfb0c --- /dev/null +++ b/src/editors/containers/library_contentEditor/data/selectors.js @@ -0,0 +1,20 @@ +import { createSelector } from 'reselect'; +import { blockTypes } from '../../constants/app'; +import * as urls from '../../services/cms/urls'; +import * as module from './selectors'; + +export const appSelector = (state) => state.app; + +const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb); + +export const simpleSelectors = { + libraries: mkSimpleSelector(app => app.libraries), + selectedLibrary: mkSimpleSelector(app => app.selectedLibrary), + selectionMode: mkSimpleSelector(app => app.selectionMode), + selectionSettings: mkSimpleSelector(app => app.selectionSettings), + blocksInSelectedLibrary: mkSimpleSelector(app => app.blocksInSelectedLibrary), +} + +export default { + ...simpleSelectors, +} \ No newline at end of file diff --git a/src/editors/containers/library_contentEditor/data/urls.js b/src/editors/containers/library_contentEditor/data/urls.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/editors/containers/library_contentEditor/index.jsx b/src/editors/containers/library_contentEditor/index.jsx new file mode 100644 index 000000000..4a4167b16 --- /dev/null +++ b/src/editors/containers/library_contentEditor/index.jsx @@ -0,0 +1,111 @@ +/* eslint-disable import/extensions */ +/* eslint-disable import/no-unresolved */ +/** + * This is an example component for an xblock Editor + * It uses pre-existing components to handle the saving of a the result of a function into the xblock's data. + * To use run npm run-script addXblock + */ + +/* eslint-disable no-unused-vars */ + +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import { Spinner } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import EditorContainer from '../EditorContainer'; +import * as module from '.'; +import { actions, selectors } from '../../data/redux'; +import { RequestKeys } from '../../data/constants/requests'; +import LibrarySelector from './LibrarySelector'; +import LibraryBlockPicker from './LibraryBlockPicker'; + +export const hooks = { + getContent: () => ({ + some: 'content', + }), +}; + +export const thumbEditor = ({ + onClose, + // redux app layer + blockValue, + lmsEndpointUrl, + blockFailed, + blockFinished, + initializeEditor, + // inject + intl, +}) => +{ + const libraries, setLibraries = useState([]); + + useEffect(() => { + setLibraries(api.getLibraries(cmsEndpointUrl)); + }, []); + + return ( + +
+ {!blockFinished + ? ( +
+ +
+ ) + : ( +
+ + { + selected_library ? + (): + (<>) + } +
+ )} +
+
+)}; +thumbEditor.defaultProps = { + blockValue: null, + lmsEndpointUrl: null, +}; +thumbEditor.propTypes = { + onClose: PropTypes.func.isRequired, + // redux + blockValue: PropTypes.shape({ + data: PropTypes.shape({ data: PropTypes.string }), + }), + lmsEndpointUrl: PropTypes.string, + blockFailed: PropTypes.bool.isRequired, + blockFinished: PropTypes.bool.isRequired, + initializeEditor: PropTypes.func.isRequired, + // inject + intl: intlShape.isRequired, +}; + +export const mapStateToProps = (state) => ({ + blockValue: selectors.app.blockValue(state), + lmsEndpointUrl: selectors.app.lmsEndpointUrl(state), + blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }), + blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }), +}); + +export const mapDispatchToProps = { + initializeEditor: actions.app.initializeEditor, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(thumbEditor)); diff --git a/src/editors/containers/library_contentEditor/messages.js b/src/editors/containers/library_contentEditor/messages.js new file mode 100644 index 000000000..46a50345e --- /dev/null +++ b/src/editors/containers/library_contentEditor/messages.js @@ -0,0 +1,22 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + modeAll: { + id: 'authoring.library_content.mode.all', + defaultMessage: 'Show all content from the library to every learner', + description: 'mode of selecting content from a library to put in a course', + }, + modeRandom: { + id: 'authoring.library_content.mode.random', + defaultMessage: 'Show X problems at random from the Library', + description: 'mode of selecting content from a library to put in a course', + }, + modeSelected: { + id: 'authoring.library_content.mode.selected', + defaultMessage: 'Show a specfic portion of the library to all users', + description: 'mode of selecting content from a library to put in a course', + }, + +}); + +export default messages; \ No newline at end of file diff --git a/src/editors/data/constants/app.js b/src/editors/data/constants/app.js index 3b0fc7b9c..bdebaf4fe 100644 --- a/src/editors/data/constants/app.js +++ b/src/editors/data/constants/app.js @@ -5,6 +5,8 @@ export const blockTypes = StrictDict({ html: 'html', video: 'video', problem: 'problem', + library_content: 'library_content', + // ADDED_EDITORS GO BELOW video_upload: 'video_upload', game: 'game', diff --git a/src/editors/supportedEditors.js b/src/editors/supportedEditors.js index 82d9c568a..f1d6401c2 100644 --- a/src/editors/supportedEditors.js +++ b/src/editors/supportedEditors.js @@ -4,6 +4,8 @@ import ProblemEditor from './containers/ProblemEditor'; import VideoUploadEditor from './containers/VideoUploadEditor'; import GameEditor from './containers/GameEditor'; +import library_contentEditor from './containers/library_contentEditor' + // ADDED_EDITOR_IMPORTS GO HERE import { blockTypes } from './data/constants/app'; @@ -13,6 +15,8 @@ const supportedEditors = { [blockTypes.video]: VideoEditor, [blockTypes.problem]: ProblemEditor, [blockTypes.video_upload]: VideoUploadEditor, + [blockTypes.library_content]: library_contentEditor, + // ADDED_EDITORS GO BELOW [blockTypes.game]: GameEditor, }; From 1a3bc9ba36f108c4dc15cadb2094e62dc7516fa1 Mon Sep 17 00:00:00 2001 From: rayzhou-bit Date: Wed, 25 Oct 2023 18:57:46 -0400 Subject: [PATCH 02/46] feat: wip --- .../LibraryContentEditor/BlocksSelector.jsx | 71 +++++++++++ .../LibraryContentEditor/LibrarySelector.jsx | 63 ++++++++++ .../LibraryContentEditor/LibrarySettings.jsx | 60 ++++++++++ .../constants.js | 8 +- .../LibraryContentEditor/data/api.js | 29 +++++ .../LibraryContentEditor/data/index.js | 2 + .../LibraryContentEditor/data/mockApi.js | 108 +++++++++++++++++ .../LibraryContentEditor/data/reducer.js | 65 ++++++++++ .../LibraryContentEditor/data/selectors.js | 20 ++++ .../LibraryContentEditor/data/urls.js | 11 ++ .../containers/LibraryContentEditor/hooks.js | 61 ++++++++++ .../containers/LibraryContentEditor/index.jsx | 107 +++++++++++++++++ .../messages.js | 0 .../BlockSettingsEditor.jsx | 88 -------------- .../library_contentEditor/LibrarySelector.jsx | 35 ------ .../library_contentEditor/data/api.js | 0 .../library_contentEditor/data/reducers.js | 51 -------- .../library_contentEditor/data/selectors.js | 20 ---- .../library_contentEditor/data/urls.js | 0 .../library_contentEditor/index.jsx | 111 ------------------ src/editors/data/redux/app/selectors.js | 2 +- src/editors/data/redux/index.js | 2 + src/editors/data/services/cms/api.js | 14 +++ src/editors/data/services/cms/mockApi.js | 18 +++ src/editors/supportedEditors.js | 5 +- 25 files changed, 638 insertions(+), 313 deletions(-) create mode 100644 src/editors/containers/LibraryContentEditor/BlocksSelector.jsx create mode 100644 src/editors/containers/LibraryContentEditor/LibrarySelector.jsx create mode 100644 src/editors/containers/LibraryContentEditor/LibrarySettings.jsx rename src/editors/containers/{library_contentEditor => LibraryContentEditor}/constants.js (76%) create mode 100644 src/editors/containers/LibraryContentEditor/data/api.js create mode 100644 src/editors/containers/LibraryContentEditor/data/index.js create mode 100644 src/editors/containers/LibraryContentEditor/data/mockApi.js create mode 100644 src/editors/containers/LibraryContentEditor/data/reducer.js create mode 100644 src/editors/containers/LibraryContentEditor/data/selectors.js create mode 100644 src/editors/containers/LibraryContentEditor/data/urls.js create mode 100644 src/editors/containers/LibraryContentEditor/hooks.js create mode 100644 src/editors/containers/LibraryContentEditor/index.jsx rename src/editors/containers/{library_contentEditor => LibraryContentEditor}/messages.js (100%) delete mode 100644 src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx delete mode 100644 src/editors/containers/library_contentEditor/LibrarySelector.jsx delete mode 100644 src/editors/containers/library_contentEditor/data/api.js delete mode 100644 src/editors/containers/library_contentEditor/data/reducers.js delete mode 100644 src/editors/containers/library_contentEditor/data/selectors.js delete mode 100644 src/editors/containers/library_contentEditor/data/urls.js delete mode 100644 src/editors/containers/library_contentEditor/index.jsx diff --git a/src/editors/containers/LibraryContentEditor/BlocksSelector.jsx b/src/editors/containers/LibraryContentEditor/BlocksSelector.jsx new file mode 100644 index 000000000..2556c61c2 --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/BlocksSelector.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import { Button, DataTable, Form } from '@edx/paragon'; +import { actions, selectors } from './data'; +import { useBlocksHook } from './hooks'; + +export const BlocksSelector = ({ + studioEndpointUrl, + + // redux + blocksInSelectedLibrary, + loadBlocksInLibrary, + selectedLibrary, +}) => { + if (selectedLibrary === null || !blocksInSelectedLibrary) return <>; + + const { + blockLinks, + blocksTableData, + } = useBlocksHook({ + blocksInSelectedLibrary, + loadBlocksInLibrary, + studioEndpointUrl, + }); + + return ( +
+ + { + // + // } + // } + // ]} + > + + + +
+ ); +}; + +export const mapStateToProps = (state) => ({ + blocksInSelectedLibrary: selectors.blocksInSelectedLibrary(state), + selectedLibrary: selectors.selectedLibrary(state), +}) + +export const mapDispatchToProps = { + loadBlocksInLibrary: actions.loadBlocksInLibrary, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(BlocksSelector)); diff --git a/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx b/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx new file mode 100644 index 000000000..08f4a9c8c --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { connect, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; + +import { Dropdown, DropdownButton } from '@edx/paragon'; +import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { actions, selectors } from './data'; + +export const LibrarySelector = ({ + // redux + libraries, + selectedLibrary, + onSelectLibrary, +}) => { + const title = () => { + if (selectedLibrary === null) return 'Select a library'; + return libraries[selectedLibrary]?.display_name; + }; + + return ( +
+ {libraries + ? ( + + + {title()} + + + onSelectLibrary({ selectedLibrary: null })}> + Select a library + + {libraries.map((library, index) => ( + onSelectLibrary({ selectedLibrary: index })}> + {library.display_name} + + ))} + + + ) + : ( + There is no library! + )} +
+ ); +}; + +export const mapStateToProps = (state) => ({ + libraries: selectors.libraries(state), + selectedLibrary: selectors.selectedLibrary(state), +}); + +export const mapDispatchToProps = { + onSelectLibrary: actions.onSelectLibrary, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibrarySelector)); + + diff --git a/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx b/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx new file mode 100644 index 000000000..8eda188bb --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import { Button, Form } from '@edx/paragon'; +import { modes } from './constants'; +import { actions, selectors } from './data'; +import { thunkActions } from '../../data/redux'; + +export const LibrarySettings = ({ + // redux + onShowResetSettingsChange, + onCountSettingsChange, + selectedLibrary, + selectionSettings, +}) => { + if (selectedLibrary === null) return <>; + + return ( +
+
+ onCountSettingsChange({ + count: e.target.value, + })} + value={selectionSettings.count} + type="number" + /> + +
+
+ onShowResetSettingsChange({ + showReset: e.target.checked, + })} + > + Show Reset Button + +
+ Determines whether a 'Reset Problems' button is shown, so users may reset their answers and reshuffle selected items. +
+
+
+ ); +}; + +export const mapStateToProps = (state) => ({ + selectedLibrary: selectors.selectedLibrary(state), + selectionSettings: selectors.selectionSettings(state), +}) + +export const mapDispatchToProps = { + onShowResetSettingsChange: actions.onShowResetSettingsChange, + onCountSettingsChange: actions.onCountSettingsChange, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibrarySettings)); diff --git a/src/editors/containers/library_contentEditor/constants.js b/src/editors/containers/LibraryContentEditor/constants.js similarity index 76% rename from src/editors/containers/library_contentEditor/constants.js rename to src/editors/containers/LibraryContentEditor/constants.js index ee23162e8..cbeb04622 100644 --- a/src/editors/containers/library_contentEditor/constants.js +++ b/src/editors/containers/LibraryContentEditor/constants.js @@ -3,14 +3,14 @@ import messages from "./messages" export const modes = { all: { description: messages.modeAll, - value: 'all' + value: 'all', }, random: { description: messages.modeRandom, - value: 'random' + value: 'random', }, selected: { description: messages.modeSelected, - value: 'selected' + value: 'selected', } -} +}; diff --git a/src/editors/containers/LibraryContentEditor/data/api.js b/src/editors/containers/LibraryContentEditor/data/api.js new file mode 100644 index 000000000..807ade1d0 --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/data/api.js @@ -0,0 +1,29 @@ +import { get, post, deleteObject } from '../../../data/services/cms/utils'; +// import * as urls from '../../../data/services/cms/urls'; +import * as urls from './urls'; +import * as module from './api'; +import * as mockApi from './mockApi'; + +export const apiMethods = { + // fetchLibraries: ({}) => get( + // urls + // ), + fetchContentStore: ({ studioEndpointUrl }) => get( + urls.contentStore({ studioEndpointUrl }), + ), + fetchLibraryContent: ({ studioEndpointUrl, libraryId }) => get( + urls.libraryContent({ studioEndpointUrl, libraryId }), + ), +}; + +export const checkMockApi = (key) => { + if (process.env.REACT_APP_DEVGALLERY) { + return mockApi[key] ? mockApi[key] : mockApi.emptyMock; + } + return module.apiMethods[key]; +}; + +export default Object.keys(apiMethods).reduce( + (obj, key) => ({ ...obj, [key]: checkMockApi(key) }), + {}, +); diff --git a/src/editors/containers/LibraryContentEditor/data/index.js b/src/editors/containers/LibraryContentEditor/data/index.js new file mode 100644 index 000000000..8abd5f91d --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/data/index.js @@ -0,0 +1,2 @@ +export { actions, reducer } from './reducer'; +export { default as selectors } from './selectors'; diff --git a/src/editors/containers/LibraryContentEditor/data/mockApi.js b/src/editors/containers/LibraryContentEditor/data/mockApi.js new file mode 100644 index 000000000..2bd1a4539 --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/data/mockApi.js @@ -0,0 +1,108 @@ +export const fetchContentStore = ({ studioEndpointUrl }) => { + return { + "allow_course_reruns": true, + "allow_to_create_new_org": true, + "allow_unicode_course_id": false, + "allowed_organizations": [], + "archived_courses": [ + { + "course_key": "course-v1:edX+P315+2T2023", + "display_name": "Quantum Entanglement", + "lms_link": "//localhost:18000/courses/course-v1:edX+P315+2T2023", + "number": "P315", + "org": "edX", + "rerun_link": "/course_rerun/course-v1:edX+P315+2T2023", + "run": "2T2023", + "url": "/course/course-v1:edX+P315+2T2023", + }, + ], + "can_create_organizations": true, + "course_creator_status": "granted", + "courses": [ + { + "course_key": "course-v1:edX+E2E-101+course", + "display_name": "E2E Test Course", + "lms_link": "//localhost:18000/courses/course-v1:edX+E2E-101+course", + "number": "E2E-101", + "org": "edX", + "rerun_link": "/course_rerun/course-v1:edX+E2E-101+course", + "run": "course", + "url": "/course/course-v1:edX+E2E-101+course" + }, + ], + "in_process_course_actions": [], + "libraries": [ + { + "display_name": "My First Library", + "library_key": "library-v1:new+CPSPR", + "url": "/library/library-v1:new+CPSPR", + "org": "new", + "number": "CPSPR", + "can_edit": true + }, + { + "display_name": "My Second Library", + "library_key": "library-v1:new+CPSPRsadf", + "url": "/library/library-v1:new+CPSPRasdf", + "org": "new", + "number": "CPSPRasdf", + "can_edit": true + }, + { + "display_name": "My Third Library", + "library_key": "library-v1:new+CPSPRqwer", + "url": "/library/library-v1:new+CPSPRqwer", + "org": "new", + "number": "CPSPRqwer", + "can_edit": true + } + ], + "libraries_enabled": true, + "library_authoring_mfe_url": "//localhost:3001/course/course-v1:edX+P315+2T2023", + "optimization_enabled": true, + "redirect_to_library_authoring_mfe": false, + "request_course_creator_url": "/request_course_creator", + "rerun_creator_status": true, + "show_new_library_button": true, + "split_studio_home": false, + "studio_name": "Studio", + "studio_short_name": "Studio", + "studio_request_email": "", + "tech_support_email": "technical@example.com", + "platform_name": "Your Platform Name Here", + "user_is_active": true, + }; +}; + +export const fetchLibraryContent = ({ studioEndpointUrl, libraryId }) => { + return { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "Text", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "Lorem texts", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "Blank Problem", + "has_unpublished_changes": true + } + ] + }; +}; + +export const emptyMock = () => mockPromise({}); diff --git a/src/editors/containers/LibraryContentEditor/data/reducer.js b/src/editors/containers/LibraryContentEditor/data/reducer.js new file mode 100644 index 000000000..de8d31483 --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/data/reducer.js @@ -0,0 +1,65 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { modes } from '../constants'; +import { StrictDict } from '../../../utils'; + +const initialState = { + libraries: [], + selectedLibrary: null, + selectionMode: modes.all, + selectionSettings: { + count: false, + showReset: false, + //max count? + }, + blocksInSelectedLibrary: [], +}; + +const library = createSlice({ + name: 'library', + initialState, + reducers: { + initialize: (state, { payload }) => ({ + ...state, + libraries: payload.libraries, + selectedLibrary: payload.selectedLibrary, + selectionMode: payload.selectionMode, + selectionSettings: payload.selectionSettings, + }), + onSelectLibrary: (state, { payload }) => ({ + ...state, + selectedLibrary: payload.selectedLibrary, + }), + onSelectionModeChange: (state, { payload }) => ({ + ...state, + selectionMode: payload.selectionMode, + }), + onShowResetSettingsChange: (state, { payload }) => ({ + ...state, + selectionSettings: { + ...state.selectionSettings, + showReset: payload.showReset, + }, + }), + onCountSettingsChange: (state, { payload }) => ({ + ...state, + selectionSettings: { + ...state.selectionSettings, + count: payload.count, + }, + }), + loadBlocksInLibrary: (state, { payload }) => ({ + ...state, + blocksInSelectedLibrary: payload.blocks, + }), + }, +}); + +const actions = StrictDict(library.actions); + +const { reducer } = library; + +export { + actions, + initialState, + reducer, +}; diff --git a/src/editors/containers/LibraryContentEditor/data/selectors.js b/src/editors/containers/LibraryContentEditor/data/selectors.js new file mode 100644 index 000000000..d537377cf --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/data/selectors.js @@ -0,0 +1,20 @@ +import { createSelector } from 'reselect'; +// import { blockTypes } from '../../constants/app'; +// import * as urls from '../../services/cms/urls'; +import * as module from './selectors'; + +export const libraryState = (state) => state.library; + +const mkSimpleSelector = (cb) => createSelector([module.libraryState], cb); + +export const simpleSelectors = { + libraries: mkSimpleSelector(library => library.libraries), + selectedLibrary: mkSimpleSelector(library => library.selectedLibrary), + selectionMode: mkSimpleSelector(library => library.selectionMode), + selectionSettings: mkSimpleSelector(library => library.selectionSettings), + blocksInSelectedLibrary: mkSimpleSelector(library => library.blocksInSelectedLibrary), +}; + +export default { + ...simpleSelectors, +}; \ No newline at end of file diff --git a/src/editors/containers/LibraryContentEditor/data/urls.js b/src/editors/containers/LibraryContentEditor/data/urls.js new file mode 100644 index 000000000..941463877 --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/data/urls.js @@ -0,0 +1,11 @@ +export const contentStore = ({ studioEndpointUrl }) => ( + `${studioEndpointUrl}/api/libraries/v1/home/` +); + +export const libraryContent = ({ studioEndpointUrl, libraryId }) => ( + `${studioEndpointUrl}/api/libraries/v2/${libraryId}/blocks/` +); + +export const blockContent = ({ studioEndpointUrl, blockId }) => ( + `${studioEndpointUrl}/library/${blockId}/` +); diff --git a/src/editors/containers/LibraryContentEditor/hooks.js b/src/editors/containers/LibraryContentEditor/hooks.js new file mode 100644 index 000000000..2576e1774 --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/hooks.js @@ -0,0 +1,61 @@ +import React, { useEffect } from 'react'; +import api from './data/api'; +import * as urls from './data/urls'; + +export const useLibraryHook = ({ + blockValue, + initialize, + studioEndpointUrl, +}) => { + useEffect(() => { + const contentStore = api.fetchContentStore({ studioEndpointUrl }); + initialize({ + libraries: contentStore.libraries, + selectedLibrary: 0, + selectionMode: 'mode', + selectionSettings: { + showReset: blockValue?.data?.metadata?.allow_resetting_children, + count: 1, + }, + }); + }, []); + + return { + getContent: () => ({ + some: 'content' + }), + }; +}; + +export const useBlocksHook = ({ + blocksInSelectedLibrary, + loadBlocksInLibrary, + selectedLibrary, + studioEndpointUrl, +}) => { + useEffect(() => { + if (selectedLibrary !== null) { + const libraryContent = api.fetchLibraryContent({ studioEndpointUrl, selectedLibrary }); + loadBlocksInLibrary({ + blocks: libraryContent.results, + }); + } + }, [selectedLibrary]); + + const blockTypeDisplay = (type) => { + if (type === 'html') return 'Text'; + if (type === 'video') return 'Video'; + if (type === 'problem') return 'Problem'; + return 'Other'; + }; + + return ({ + blockLinks: blocksInSelectedLibrary.map(block => ( + urls.blockContent({ studioEndpointUrl, blockId: block.url }) + )), + blocksTableData: blocksInSelectedLibrary.map(block => ({ + display_name: block.display_name, + block_type: blockTypeDisplay(block.block_type), + })), + }); +}; diff --git a/src/editors/containers/LibraryContentEditor/index.jsx b/src/editors/containers/LibraryContentEditor/index.jsx new file mode 100644 index 000000000..3ad9ed463 --- /dev/null +++ b/src/editors/containers/LibraryContentEditor/index.jsx @@ -0,0 +1,107 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import { Spinner } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import EditorContainer from '../EditorContainer'; +import { useLibraryHook } from './hooks'; +import { actions, selectors } from '../../data/redux'; +import { RequestKeys } from '../../data/constants/requests'; +import LibrarySelector from './LibrarySelector'; +import LibrarySettings from './LibrarySettings'; +import BlocksSelector from './BlocksSelector'; + +export const thumbEditor = ({ + onClose, + // redux app layer + blockValue, + blockFailed, + blockFinished, + initialize, + lmsEndpointUrl, + studioEndpointUrl, + // inject + intl, +}) => { + const { + getContent, + } = useLibraryHook({ + blockValue, + initialize, + studioEndpointUrl, + }); + + const loading = () => { + return ( +
+ +
+ ); + }; + + const loaded = () => { + return ( +
+ + + +
+ ); + }; + + return ( + +
+ {!blockFinished + ? loading() + : loaded() + } +
+
+ ); +}; + +thumbEditor.defaultProps = { + blockValue: null, + lmsEndpointUrl: null, +}; + +thumbEditor.propTypes = { + onClose: PropTypes.func.isRequired, + // redux + blockValue: PropTypes.shape({ + data: PropTypes.shape({ data: PropTypes.string }), + }), + lmsEndpointUrl: PropTypes.string, + blockFailed: PropTypes.bool.isRequired, + blockFinished: PropTypes.bool.isRequired, + initializeEditor: PropTypes.func.isRequired, + // inject + intl: intlShape.isRequired, +}; + +export const mapStateToProps = (state) => ({ + blockValue: selectors.app.blockValue(state), + lmsEndpointUrl: selectors.app.lmsEndpointUrl(state), + blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }), + blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }), + studioEndpointUrl: selectors.app.studioEndpointUrl(state), +}); + +export const mapDispatchToProps = { + initialize: actions.library.initialize, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(thumbEditor)); diff --git a/src/editors/containers/library_contentEditor/messages.js b/src/editors/containers/LibraryContentEditor/messages.js similarity index 100% rename from src/editors/containers/library_contentEditor/messages.js rename to src/editors/containers/LibraryContentEditor/messages.js diff --git a/src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx b/src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx deleted file mode 100644 index 987ad9d6c..000000000 --- a/src/editors/containers/library_contentEditor/BlockSettingsEditor.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import { modes } from './constants'; -import { selectors, thunkActions } from '../../data/redux'; - -const BlockSettingsEditor = ({ - selectionMode, - onSelectionModeChange, - selectionSettings, - blocksInSelectedLibrary, - onSelectionSettingsChange, - }) => { - - const getSelectionSettings = () =>{ - if (selectionMode === modes.all){ - return (<>) - } - if (selectionMode === modes.random){ - return ( - <> - - onSelectionSettingsChange({ - count: e.target.value, - ...selectionSettings - })} - floatingLabel="How many blocks do you want to show the author?" - /> - - - onSelectionSettingsChange({ - showReset: e.target.checked, - ...selectionSettings - })} - > - Show Reset Button - -
- Determines whether a 'Reset Problems' button is shown, so users may reset their answers and reshuffle selected items. -
-
- {/* TODO: ADD CAPA FILTERING FOR V1 ONLY */} - - ) - } - if (selectionMode === modes.selected){ - return ( - <> - {/*TODO: ADD BLOCK PICKER*/} -

Block Selection Can be Made by Saving the editor and clicking the "view" button or going here.

- {/* Opens library page in new window.*/} - https://studio.edx.org/container/block-v1:edX+LA101+2022_Summer+type@library_content+block@6a0e7d3c67614ae78e28d575408624cf - - ) - } - } - return ( -
- - - { - modes.values().map(mode => ()) - } - - {getSelectionSettings()} - -
- ); -}; - -export const mapStateToProps = (state) => ({ - selectionMode: selectors.library.selectionMode(state), - selectionSettings: selectors.library.selectionSettings(state), - blocksInSelectedLibrary: selectors.library.blocksInSelectedLibrary(state), -}) - -export const mapDispatchToProps = { - onSelectionModeChange: thunkActions.library.onSelectionModeChange, - onSelectionSettingsChange: thunkActions.library.onSelectionSettingsChange, -}; - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibraryBlockPicker)); \ No newline at end of file diff --git a/src/editors/containers/library_contentEditor/LibrarySelector.jsx b/src/editors/containers/library_contentEditor/LibrarySelector.jsx deleted file mode 100644 index e22f91510..000000000 --- a/src/editors/containers/library_contentEditor/LibrarySelector.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { Dropdown } from '@edx/paragon'; - - -const LibrarySelector = ({ libraries, selectedLibrary, onSelectLibrary }) => { - return ( -
- - ({ - label: library.name, - value: library.id, - }))} - value={selectedLibrary} - onChange={onSelectLibrary} - /> -
- ); -}; - -export const mapStateToProps = (state) => ({ - selectionMode: selectors.library.libraries(state), - selectionSettings: selectors.library.selectedLibrary(state), -}) - -export const mapDispatchToProps = { - onSelectLibrary: reducers.library.onSelectLibrary, -}; - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibrarySelector)); - - diff --git a/src/editors/containers/library_contentEditor/data/api.js b/src/editors/containers/library_contentEditor/data/api.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/editors/containers/library_contentEditor/data/reducers.js b/src/editors/containers/library_contentEditor/data/reducers.js deleted file mode 100644 index 3e4155f19..000000000 --- a/src/editors/containers/library_contentEditor/data/reducers.js +++ /dev/null @@ -1,51 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; -import { modes } from '../constants'; - -const initialState = { - libraries: [], - selectedLibrary: null, - selectionMode: modes.all, - selectionSettings: { - count: false, - showReset: false, - }, - blocksInSelectedLibrary: [], -}; - -const library = createSlice({ - name: 'library', - initialState, - reducers: { - initialize: (state, { payload }) => ({ - ...state, - studioEndpointUrl: payload.studioEndpointUrl, - lmsEndpointUrl: payload.lmsEndpointUrl, - blockId: payload.blockId, - learningContextId: payload.learningContextId, - blockType: payload.blockType, - blockValue: null, - }), - onSelectLibrary: (state, {payload}) => ({ - ...state, - selectedLibrary: payload.selectedLibrary, - }), - onSelectionModeChange: (state, {payload}) => ({ - ...state, - selectionMode: payload.selectionMode, - }), - onSelectionSettingsChange: (state,{payload})=>({ - ...state, - selectionSettings:payload.selectionSettings, - }), - }, - }); - -const actions = StrictDict(app.actions); - -const { reducer } = library; - -export { - actions, - initialState, - reducer, -}; \ No newline at end of file diff --git a/src/editors/containers/library_contentEditor/data/selectors.js b/src/editors/containers/library_contentEditor/data/selectors.js deleted file mode 100644 index fb5fbfb0c..000000000 --- a/src/editors/containers/library_contentEditor/data/selectors.js +++ /dev/null @@ -1,20 +0,0 @@ -import { createSelector } from 'reselect'; -import { blockTypes } from '../../constants/app'; -import * as urls from '../../services/cms/urls'; -import * as module from './selectors'; - -export const appSelector = (state) => state.app; - -const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb); - -export const simpleSelectors = { - libraries: mkSimpleSelector(app => app.libraries), - selectedLibrary: mkSimpleSelector(app => app.selectedLibrary), - selectionMode: mkSimpleSelector(app => app.selectionMode), - selectionSettings: mkSimpleSelector(app => app.selectionSettings), - blocksInSelectedLibrary: mkSimpleSelector(app => app.blocksInSelectedLibrary), -} - -export default { - ...simpleSelectors, -} \ No newline at end of file diff --git a/src/editors/containers/library_contentEditor/data/urls.js b/src/editors/containers/library_contentEditor/data/urls.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/editors/containers/library_contentEditor/index.jsx b/src/editors/containers/library_contentEditor/index.jsx deleted file mode 100644 index 4a4167b16..000000000 --- a/src/editors/containers/library_contentEditor/index.jsx +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable import/extensions */ -/* eslint-disable import/no-unresolved */ -/** - * This is an example component for an xblock Editor - * It uses pre-existing components to handle the saving of a the result of a function into the xblock's data. - * To use run npm run-script addXblock - */ - -/* eslint-disable no-unused-vars */ - -import React, { useState } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; - -import { Spinner } from '@edx/paragon'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - -import EditorContainer from '../EditorContainer'; -import * as module from '.'; -import { actions, selectors } from '../../data/redux'; -import { RequestKeys } from '../../data/constants/requests'; -import LibrarySelector from './LibrarySelector'; -import LibraryBlockPicker from './LibraryBlockPicker'; - -export const hooks = { - getContent: () => ({ - some: 'content', - }), -}; - -export const thumbEditor = ({ - onClose, - // redux app layer - blockValue, - lmsEndpointUrl, - blockFailed, - blockFinished, - initializeEditor, - // inject - intl, -}) => -{ - const libraries, setLibraries = useState([]); - - useEffect(() => { - setLibraries(api.getLibraries(cmsEndpointUrl)); - }, []); - - return ( - -
- {!blockFinished - ? ( -
- -
- ) - : ( -
- - { - selected_library ? - (): - (<>) - } -
- )} -
-
-)}; -thumbEditor.defaultProps = { - blockValue: null, - lmsEndpointUrl: null, -}; -thumbEditor.propTypes = { - onClose: PropTypes.func.isRequired, - // redux - blockValue: PropTypes.shape({ - data: PropTypes.shape({ data: PropTypes.string }), - }), - lmsEndpointUrl: PropTypes.string, - blockFailed: PropTypes.bool.isRequired, - blockFinished: PropTypes.bool.isRequired, - initializeEditor: PropTypes.func.isRequired, - // inject - intl: intlShape.isRequired, -}; - -export const mapStateToProps = (state) => ({ - blockValue: selectors.app.blockValue(state), - lmsEndpointUrl: selectors.app.lmsEndpointUrl(state), - blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }), - blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }), -}); - -export const mapDispatchToProps = { - initializeEditor: actions.app.initializeEditor, -}; - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(thumbEditor)); diff --git a/src/editors/data/redux/app/selectors.js b/src/editors/data/redux/app/selectors.js index 870739781..98ef449f1 100644 --- a/src/editors/data/redux/app/selectors.js +++ b/src/editors/data/redux/app/selectors.js @@ -52,7 +52,7 @@ export const displayTitle = createSelector( if (blockType === null) { return null; } - if (blockTitle !== null) { + if (blockTitle) { return blockTitle; } return (blockType === blockTypes.html) diff --git a/src/editors/data/redux/index.js b/src/editors/data/redux/index.js index 05811bbf2..2cb2ae3a8 100644 --- a/src/editors/data/redux/index.js +++ b/src/editors/data/redux/index.js @@ -7,6 +7,7 @@ import * as requests from './requests'; import * as video from './video'; import * as problem from './problem'; import * as game from './game'; +import * as library from '../../containers/LibraryContentEditor/data'; /* eslint-disable import/no-cycle */ export { default as thunkActions } from './thunkActions'; @@ -17,6 +18,7 @@ const modules = { video, problem, game, + library, }; const moduleProps = (propName) => Object.keys(modules).reduce( diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js index b1e407ad6..d8f0dcab1 100644 --- a/src/editors/data/services/cms/api.js +++ b/src/editors/data/services/cms/api.js @@ -180,6 +180,20 @@ export const apiMethods = { license: module.processLicense(content.licenseType, content.licenseDetails), }, }; + } else if (blockType ==='library_content') { + response = { + category: blockType, + courseKey: learningContextId, + display_name: title, //"course-v1:edx+123+test" + has_children: "TODO TRUE FALSE", + highlights: [], + highlights_doc_url: "", + highlights_enabled: false, + highlights_enabled_for_messaging: false, + highlights_preview_only: true, + id: blockId, //"block-v1:edx+123+test+type@library_content+block@880758e62c2542d5b45c944360dfa799" + metadata: {}, + }; } else { throw new TypeError(`No Block in V2 Editors named /"${blockType}/", Cannot Save Content.`); } diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js index 2353bb748..0a19a5e4f 100644 --- a/src/editors/data/services/cms/mockApi.js +++ b/src/editors/data/services/cms/mockApi.js @@ -283,6 +283,24 @@ export const fetchStudioView = ({ blockId, studioEndpointUrl }) => { weight: 29, }, }; + } else if (blockId === 'library_content-block-id') { + data = { + id: 'mock-block-id', + data: null, + display_name: 'library title', + has_children: false, + highlights: [], + highlights_doc_url: "", + highlights_enabled: false, + highlights_enabled_for_messaging: false, + highlights_preview_only: true, + metadata: { + allow_resetting_children: false, + capa_type: "any", + source_library_id: "library-v1:BradenX+LIB100", + source_library_version: "6537dc2ab6ab347e6266e780" + }, + }; } return mockPromise({ diff --git a/src/editors/supportedEditors.js b/src/editors/supportedEditors.js index f1d6401c2..83f178543 100644 --- a/src/editors/supportedEditors.js +++ b/src/editors/supportedEditors.js @@ -3,8 +3,7 @@ import VideoEditor from './containers/VideoEditor'; import ProblemEditor from './containers/ProblemEditor'; import VideoUploadEditor from './containers/VideoUploadEditor'; import GameEditor from './containers/GameEditor'; - -import library_contentEditor from './containers/library_contentEditor' +import LibraryContentEditor from './containers/LibraryContentEditor' // ADDED_EDITOR_IMPORTS GO HERE @@ -15,7 +14,7 @@ const supportedEditors = { [blockTypes.video]: VideoEditor, [blockTypes.problem]: ProblemEditor, [blockTypes.video_upload]: VideoUploadEditor, - [blockTypes.library_content]: library_contentEditor, + [blockTypes.library_content]: LibraryContentEditor, // ADDED_EDITORS GO BELOW [blockTypes.game]: GameEditor, From 668c184afd6d6f2dcbe4c9f9eabe5fb098aab059 Mon Sep 17 00:00:00 2001 From: rayzhou-bit Date: Wed, 1 Nov 2023 16:03:12 -0400 Subject: [PATCH 03/46] feat: wip2 --- .../LibraryContentEditor/BlocksSelector.jsx | 86 ++++-- .../LibraryContentEditor/LibrarySelector.jsx | 2 +- .../LibraryContentEditor/LibrarySettings.jsx | 89 ++++-- .../LibraryContentEditor/constants.js | 22 +- .../LibraryContentEditor/data/mockApi.js | 253 ++++++++++++++++-- .../LibraryContentEditor/data/reducer.js | 8 +- .../containers/LibraryContentEditor/hooks.js | 50 ++-- .../containers/LibraryContentEditor/index.jsx | 6 +- .../LibraryContentEditor/messages.js | 61 +++-- src/editors/data/services/cms/api.js | 3 +- 10 files changed, 449 insertions(+), 131 deletions(-) diff --git a/src/editors/containers/LibraryContentEditor/BlocksSelector.jsx b/src/editors/containers/LibraryContentEditor/BlocksSelector.jsx index 2556c61c2..1cf0932bc 100644 --- a/src/editors/containers/LibraryContentEditor/BlocksSelector.jsx +++ b/src/editors/containers/LibraryContentEditor/BlocksSelector.jsx @@ -1,59 +1,92 @@ import React from 'react'; import { connect } from 'react-redux'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - +import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button, DataTable, Form } from '@edx/paragon'; + +import messages from './messages'; import { actions, selectors } from './data'; import { useBlocksHook } from './hooks'; +import { modes } from './constants'; export const BlocksSelector = ({ studioEndpointUrl, // redux blocksInSelectedLibrary, + libraries, loadBlocksInLibrary, + onSelectCandidates, selectedLibrary, + selectionMode, }) => { - if (selectedLibrary === null || !blocksInSelectedLibrary) return <>; - const { blockLinks, blocksTableData, + selectCandidates, } = useBlocksHook({ blocksInSelectedLibrary, loadBlocksInLibrary, + onSelectCandidates, + selectedLibraryId: (selectedLibrary !== null) ? libraries[selectedLibrary].library_key : null, studioEndpointUrl, }); + if (selectedLibrary === null || !blocksInSelectedLibrary) return <>; + + const ViewAction = ({ row }) => ( + + ); + return ( -
- +
+ { + selectionMode === modes.selected.value + ? + : null + } { - // - // } - // } - // ]} + columns={[ + { + Header: 'Name', + accessor: 'display_name', + }, + { + Header: 'Block Type', + accessor: 'block_type', + }, + ]} + additionalColumns={[ + { + id: 'action', + Header: 'View', + Cell: ({ row }) => ViewAction({ row }), + } + ]} + onSelectedRowsChanged={(selected) => selectCandidates({ selected })} + // maxSelectedRows={2} + // onMaxSelectedRows={() => console.log('hey3 this is the last row allowed')} > + - +
); @@ -61,11 +94,14 @@ export const BlocksSelector = ({ export const mapStateToProps = (state) => ({ blocksInSelectedLibrary: selectors.blocksInSelectedLibrary(state), + libraries: selectors.libraries(state), selectedLibrary: selectors.selectedLibrary(state), + selectionMode: selectors.selectionMode(state), }) export const mapDispatchToProps = { loadBlocksInLibrary: actions.loadBlocksInLibrary, + onSelectCandidates: actions.onSelectCandidates, }; export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(BlocksSelector)); diff --git a/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx b/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx index 08f4a9c8c..01fa29b0a 100644 --- a/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx +++ b/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx @@ -18,7 +18,7 @@ export const LibrarySelector = ({ }; return ( -
+
{libraries ? ( diff --git a/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx b/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx index 8eda188bb..4566f4159 100644 --- a/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx +++ b/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx @@ -1,46 +1,81 @@ import React from 'react'; import { connect } from 'react-redux'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - +import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button, Form } from '@edx/paragon'; + +import messages from './messages'; import { modes } from './constants'; import { actions, selectors } from './data'; import { thunkActions } from '../../data/redux'; export const LibrarySettings = ({ // redux - onShowResetSettingsChange, onCountSettingsChange, + onSelectionModeChange, + onShowResetSettingsChange, selectedLibrary, + selectionMode, selectionSettings, }) => { if (selectedLibrary === null) return <>; return ( -
-
- onCountSettingsChange({ - count: e.target.value, - })} - value={selectionSettings.count} - type="number" - /> - -
-
- onShowResetSettingsChange({ - showReset: e.target.checked, +
+
+ onSelectionModeChange({ selectionMode: e.target.value })} + value={selectionMode} + > + + + + + + + + + {/* + } + onChange={(e) => onSelectionModeChange({ + selectionMode: (e.target.checked ? modes.selected.value : modes.random.value) })} > - Show Reset Button - -
- Determines whether a 'Reset Problems' button is shown, so users may reset their answers and reshuffle selected items. + + */} +
+ { + selectionMode === modes.random.value + ?
+ onCountSettingsChange({ + count: e.target.value, + })} + value={selectionSettings.count} + type="number" + /> + +
+ : null + } + +
+
+ onShowResetSettingsChange({ + showReset: e.target.checked, + })} + > + + +
+ +
@@ -49,12 +84,14 @@ export const LibrarySettings = ({ export const mapStateToProps = (state) => ({ selectedLibrary: selectors.selectedLibrary(state), + selectionMode: selectors.selectionMode(state), selectionSettings: selectors.selectionSettings(state), }) export const mapDispatchToProps = { - onShowResetSettingsChange: actions.onShowResetSettingsChange, onCountSettingsChange: actions.onCountSettingsChange, + onSelectionModeChange: actions.onSelectionModeChange, + onShowResetSettingsChange: actions.onShowResetSettingsChange, }; export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibrarySettings)); diff --git a/src/editors/containers/LibraryContentEditor/constants.js b/src/editors/containers/LibraryContentEditor/constants.js index cbeb04622..defbdefd2 100644 --- a/src/editors/containers/LibraryContentEditor/constants.js +++ b/src/editors/containers/LibraryContentEditor/constants.js @@ -1,16 +1,14 @@ import messages from "./messages" export const modes = { - all: { - description: messages.modeAll, - value: 'all', - }, - random: { - description: messages.modeRandom, - value: 'random', - }, - selected: { - description: messages.modeSelected, - value: 'selected', - } + random: { + title: messages.modeRandom, + description: messages.modeRandomDescription, + value: 'random', + }, + selected: { + title: messages.modeSelected, + description: messages.modeSelectedDescription, + value: 'selected', + }, }; diff --git a/src/editors/containers/LibraryContentEditor/data/mockApi.js b/src/editors/containers/LibraryContentEditor/data/mockApi.js index 2bd1a4539..5c305c831 100644 --- a/src/editors/containers/LibraryContentEditor/data/mockApi.js +++ b/src/editors/containers/LibraryContentEditor/data/mockApi.js @@ -55,7 +55,15 @@ export const fetchContentStore = ({ studioEndpointUrl }) => { "org": "new", "number": "CPSPRqwer", "can_edit": true - } + }, + { + "display_name": "Really Big Library", + "library_key": "library-v1:new+CPSPR+big", + "url": "/library/library-v1:new+CPSPR+big", + "org": "new", + "number": "CPSPR+big", + "can_edit": true + }, ], "libraries_enabled": true, "library_authoring_mfe_url": "//localhost:3001/course/course-v1:edX+P315+2T2023", @@ -75,34 +83,221 @@ export const fetchContentStore = ({ studioEndpointUrl }) => { }; export const fetchLibraryContent = ({ studioEndpointUrl, libraryId }) => { - return { - "count": 3, - "next": null, - "previous": null, - "results": [ - { - "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", - "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", - "block_type": "html", - "display_name": "Text", - "has_unpublished_changes": true - }, - { - "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", - "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", - "block_type": "html", - "display_name": "Lorem texts", - "has_unpublished_changes": false - }, - { - "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", - "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", - "block_type": "problem", - "display_name": "Blank Problem", - "has_unpublished_changes": true - } - ] - }; + if (libraryId === 'library-v1:new+CPSPR') { + return { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "Text", + "has_unpublished_changes": true + }, + ], + }; + } else if (libraryId === 'library-v1:new+CPSPRsadf') { + return { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "Text", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "Lorem texts", + "has_unpublished_changes": false + }, + ], + }; + } else if (libraryId === 'library-v1:new+CPSPRqwer') { + return { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "Text", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "Lorem texts", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "Blank Problem", + "has_unpublished_changes": true + }, + ], + }; + } else if (libraryId === 'library-v1:new+CPSPR+big') { + return { + "count": 20, + "next": null, + "previous": null, + "results": [ + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "a", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "b", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "c", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "d", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "e", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "f", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "g", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "h", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "i", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "j", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "k", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "l", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "m", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "n", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "o", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "p", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "q", + "has_unpublished_changes": false + }, + { + "id": "lb:edx:test202:problem:86e480e4-e31c-492f-822e-1a8b2501cfde", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:problem:problem/86e480e4-e31c-492f-822e-1a8b2501cfde/definition.xml", + "block_type": "problem", + "display_name": "r", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/3eb918b1-6ebd-4172-b1e7-bd7c4eaa51ab/definition.xml", + "block_type": "html", + "display_name": "s", + "has_unpublished_changes": true + }, + { + "id": "lb:edx:test202:html:b7c18081-0bec-4de0-8d16-8b255924722e", + "def_key": "bundle-olx:539d7fcc-615c-4b6e-8b57-34cffc82bbd0:studio_draft:html:html/b7c18081-0bec-4de0-8d16-8b255924722e/definition.xml", + "block_type": "html", + "display_name": "t", + "has_unpublished_changes": false + }, + ], + }; + } }; export const emptyMock = () => mockPromise({}); diff --git a/src/editors/containers/LibraryContentEditor/data/reducer.js b/src/editors/containers/LibraryContentEditor/data/reducer.js index de8d31483..d50a9eb06 100644 --- a/src/editors/containers/LibraryContentEditor/data/reducer.js +++ b/src/editors/containers/LibraryContentEditor/data/reducer.js @@ -5,13 +5,13 @@ import { StrictDict } from '../../../utils'; const initialState = { libraries: [], selectedLibrary: null, - selectionMode: modes.all, + selectionMode: modes.random.value, // 'random' or 'selected' selectionSettings: { count: false, showReset: false, - //max count? }, blocksInSelectedLibrary: [], + candidateBlocks: [], // tuples of (block_type, block_id) }; const library = createSlice({ @@ -51,6 +51,10 @@ const library = createSlice({ ...state, blocksInSelectedLibrary: payload.blocks, }), + onSelectCandidates: (state, { payload }) => ({ + ...state, + candidateBlocks: payload.candidates, + }), }, }); diff --git a/src/editors/containers/LibraryContentEditor/hooks.js b/src/editors/containers/LibraryContentEditor/hooks.js index 2576e1774..e4040e851 100644 --- a/src/editors/containers/LibraryContentEditor/hooks.js +++ b/src/editors/containers/LibraryContentEditor/hooks.js @@ -1,23 +1,28 @@ import React, { useEffect } from 'react'; +import { modes } from './constants'; import api from './data/api'; import * as urls from './data/urls'; export const useLibraryHook = ({ + blockFailed, + blockFinished, blockValue, initialize, studioEndpointUrl, }) => { useEffect(() => { - const contentStore = api.fetchContentStore({ studioEndpointUrl }); - initialize({ - libraries: contentStore.libraries, - selectedLibrary: 0, - selectionMode: 'mode', - selectionSettings: { - showReset: blockValue?.data?.metadata?.allow_resetting_children, - count: 1, - }, - }); + if (blockFinished && !blockFailed) { + const contentStore = api.fetchContentStore({ studioEndpointUrl }); + initialize({ + libraries: contentStore.libraries, + selectedLibrary: 0, + selectionMode: modes.random.value, + selectionSettings: { + showReset: blockValue?.data?.metadata?.allow_resetting_children, + count: 1, + }, + }); + } }, []); return { @@ -30,17 +35,20 @@ export const useLibraryHook = ({ export const useBlocksHook = ({ blocksInSelectedLibrary, loadBlocksInLibrary, - selectedLibrary, + onSelectCandidates, + selectedLibraryId, studioEndpointUrl, }) => { useEffect(() => { - if (selectedLibrary !== null) { - const libraryContent = api.fetchLibraryContent({ studioEndpointUrl, selectedLibrary }); + if (selectedLibraryId !== null) { + const libraryContent = api.fetchLibraryContent({ studioEndpointUrl, libraryId: selectedLibraryId }); loadBlocksInLibrary({ blocks: libraryContent.results, }); + } else { + // TODO set candidate to empty list [] } - }, [selectedLibrary]); + }, [selectedLibraryId]); const blockTypeDisplay = (type) => { if (type === 'html') return 'Text'; @@ -51,11 +59,23 @@ export const useBlocksHook = ({ return ({ blockLinks: blocksInSelectedLibrary.map(block => ( - urls.blockContent({ studioEndpointUrl, blockId: block.url }) + urls.blockContent({ + studioEndpointUrl, + blockId: block.id, + }) )), blocksTableData: blocksInSelectedLibrary.map(block => ({ display_name: block.display_name, block_type: blockTypeDisplay(block.block_type), })), + selectCandidates: ({ selected }) => { + let candidates = [] + for (const [key, value] of Object.entries(selected)) { + if (value) { + candidates.push([ blocksInSelectedLibrary[key].block_type, blocksInSelectedLibrary[key].id ]); + } + } + onSelectCandidates({ candidates }); + }, }); }; diff --git a/src/editors/containers/LibraryContentEditor/index.jsx b/src/editors/containers/LibraryContentEditor/index.jsx index 3ad9ed463..cc4bafa20 100644 --- a/src/editors/containers/LibraryContentEditor/index.jsx +++ b/src/editors/containers/LibraryContentEditor/index.jsx @@ -20,7 +20,6 @@ export const thumbEditor = ({ blockFailed, blockFinished, initialize, - lmsEndpointUrl, studioEndpointUrl, // inject intl, @@ -28,6 +27,8 @@ export const thumbEditor = ({ const { getContent, } = useLibraryHook({ + blockFailed, + blockFinished, blockValue, initialize, studioEndpointUrl, @@ -75,7 +76,6 @@ export const thumbEditor = ({ thumbEditor.defaultProps = { blockValue: null, - lmsEndpointUrl: null, }; thumbEditor.propTypes = { @@ -84,7 +84,6 @@ thumbEditor.propTypes = { blockValue: PropTypes.shape({ data: PropTypes.shape({ data: PropTypes.string }), }), - lmsEndpointUrl: PropTypes.string, blockFailed: PropTypes.bool.isRequired, blockFinished: PropTypes.bool.isRequired, initializeEditor: PropTypes.func.isRequired, @@ -94,7 +93,6 @@ thumbEditor.propTypes = { export const mapStateToProps = (state) => ({ blockValue: selectors.app.blockValue(state), - lmsEndpointUrl: selectors.app.lmsEndpointUrl(state), blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }), blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }), studioEndpointUrl: selectors.app.studioEndpointUrl(state), diff --git a/src/editors/containers/LibraryContentEditor/messages.js b/src/editors/containers/LibraryContentEditor/messages.js index 46a50345e..3e98a88ab 100644 --- a/src/editors/containers/LibraryContentEditor/messages.js +++ b/src/editors/containers/LibraryContentEditor/messages.js @@ -1,22 +1,51 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - modeAll: { - id: 'authoring.library_content.mode.all', - defaultMessage: 'Show all content from the library to every learner', - description: 'mode of selecting content from a library to put in a course', - }, - modeRandom: { - id: 'authoring.library_content.mode.random', - defaultMessage: 'Show X problems at random from the Library', - description: 'mode of selecting content from a library to put in a course', - }, - modeSelected: { - id: 'authoring.library_content.mode.selected', - defaultMessage: 'Show a specfic portion of the library to all users', - description: 'mode of selecting content from a library to put in a course', - }, - + countLabel: { + id: 'authoring.library_content.count.label', + defaultMessage: 'Enter the number of components to display to each student. Set it to -1 to display all components.', + description: 'label for number of blocks to show', + }, + modeRandom: { + id: 'authoring.library_content.mode.random', + defaultMessage: 'Random Blocks', + description: 'name of mode for selecting content from a library to put in a course', + }, + modeSelected: { + id: 'authoring.library_content.mode.selected', + defaultMessage: 'Selected Blocks', + description: 'name of mode for selecting content from a library to put in a course', + }, + modeRandomDescription: { + id: 'authoring.library_content.mode.random.description', + defaultMessage: 'Show a number of components to display at random from the library.', + description: 'description of mode for selecting content from a library to put in a course', + }, + modeSelectedDescription: { + id: 'authoring.library_content.mode.selected.description', + defaultMessage: 'Show the selected blocks of the library to all users.', + description: 'description of mode for selecting content from a library to put in a course', + }, + resetButton: { + id: 'authoring.library_content.reset.button', + defaultMessage: 'Show Reset Button', + description: 'name of button for allowing users to reset answers and reshuffle selected items', + }, + resetButtonDescription: { + id: 'authoring.library_content.reset.button.description', + defaultMessage: "Determines whether a 'Reset Problems' button is shown, so users may reset their answers and reshuffle selected items.", + description: 'description of button for allowing users to reset answers and reshuffle selected items', + }, + tableInstructionLabel: { + id: 'authoring.library_content.table.instruction.label', + defaultMessage: 'Select the components you want to display:', + description: 'label for block selection table to instruct users to select the blocks to show', + }, + tableViewButton: { + id: 'authoring.library_content.table.view.button', + defaultMessage: 'View', + description: 'button on block selection table for viewing block in a separate browser tab', + }, }); export default messages; \ No newline at end of file diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js index d8f0dcab1..c73625a29 100644 --- a/src/editors/data/services/cms/api.js +++ b/src/editors/data/services/cms/api.js @@ -185,13 +185,14 @@ export const apiMethods = { category: blockType, courseKey: learningContextId, display_name: title, //"course-v1:edx+123+test" - has_children: "TODO TRUE FALSE", + // has_children: true, highlights: [], highlights_doc_url: "", highlights_enabled: false, highlights_enabled_for_messaging: false, highlights_preview_only: true, id: blockId, //"block-v1:edx+123+test+type@library_content+block@880758e62c2542d5b45c944360dfa799" + // TODO candidates? metadata: {}, }; } else { From d8a45fd310998eba351f7e26c2cdc5a067380cd9 Mon Sep 17 00:00:00 2001 From: rayzhou-bit Date: Mon, 6 Nov 2023 04:37:38 -0500 Subject: [PATCH 04/46] feat: wip3 --- .../containers/EditorContainer/index.jsx | 2 +- .../LibraryContentEditor/BlocksSelector.jsx | 134 ++++++++++++++---- .../LibraryContentEditor/LibrarySelector.jsx | 39 +++-- .../LibraryContentEditor/LibrarySettings.jsx | 52 +++---- .../LibraryContentEditor/data/api.js | 10 +- .../LibraryContentEditor/data/mockApi.js | 2 + .../LibraryContentEditor/data/reducer.js | 103 +++++++++++--- .../LibraryContentEditor/data/selectors.js | 43 +++++- .../LibraryContentEditor/data/urls.js | 4 + .../containers/LibraryContentEditor/hooks.js | 132 ++++++++++++----- .../containers/LibraryContentEditor/index.jsx | 26 ++-- .../LibraryContentEditor/messages.js | 15 ++ src/editors/data/services/cms/api.js | 12 +- 13 files changed, 431 insertions(+), 143 deletions(-) diff --git a/src/editors/containers/EditorContainer/index.jsx b/src/editors/containers/EditorContainer/index.jsx index f1a94a827..4c596d09f 100644 --- a/src/editors/containers/EditorContainer/index.jsx +++ b/src/editors/containers/EditorContainer/index.jsx @@ -64,7 +64,7 @@ export const EditorContainer = ({ />
- + {isInitialized && children} { + if (selectedLibraryId === null) return <>; + const { blockLinks, blocksTableData, - selectCandidates, + tempCandidates, + setTempCandidates, + isSelectable, } = useBlocksHook({ blocksInSelectedLibrary, - loadBlocksInLibrary, - onSelectCandidates, - selectedLibraryId: (selectedLibrary !== null) ? libraries[selectedLibrary].library_key : null, + candidates, + mode, + onCandidatesChange, + // settings, + selectedLibraryId, studioEndpointUrl, }); - if (selectedLibrary === null || !blocksInSelectedLibrary) return <>; + const selectColumn = { + id: 'selection', + Header: DataTable.ControlledSelectHeader, + Cell: DataTable.ControlledSelect, + disableSortBy: true, + }; const ViewAction = ({ row }) => (
); }; @@ -95,13 +177,11 @@ export const BlocksSelector = ({ export const mapStateToProps = (state) => ({ blocksInSelectedLibrary: selectors.blocksInSelectedLibrary(state), libraries: selectors.libraries(state), - selectedLibrary: selectors.selectedLibrary(state), - selectionMode: selectors.selectionMode(state), -}) + selectedLibraryId: selectors.selectedLibraryId(state), +}); export const mapDispatchToProps = { - loadBlocksInLibrary: actions.loadBlocksInLibrary, - onSelectCandidates: actions.onSelectCandidates, + onCandidatesChange: actions.onCandidatesChange, }; export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(BlocksSelector)); diff --git a/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx b/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx index 01fa29b0a..d6011fd93 100644 --- a/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx +++ b/src/editors/containers/LibraryContentEditor/LibrarySelector.jsx @@ -1,21 +1,33 @@ import React from 'react'; -import { connect, useSelector } from 'react-redux'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; - -import { Dropdown, DropdownButton } from '@edx/paragon'; +import { Dropdown } from '@edx/paragon'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; import { actions, selectors } from './data'; +import { useLibrarySelectorHook } from './hooks'; export const LibrarySelector = ({ + studioEndpointUrl, // redux libraries, + loadLibrary, selectedLibrary, onSelectLibrary, + settings, + unloadLibrary, }) => { - const title = () => { - if (selectedLibrary === null) return 'Select a library'; - return libraries[selectedLibrary]?.display_name; - }; + const { + title, + } = useLibrarySelectorHook({ + libraries, + loadLibrary, + selectedLibrary, + settings, + studioEndpointUrl, + unloadLibrary, + }); return (
@@ -25,14 +37,14 @@ export const LibrarySelector = ({ - {title()} + {title} onSelectLibrary({ selectedLibrary: null })}> - Select a library + {libraries.map((library, index) => ( onSelectLibrary({ selectedLibrary: index })}> @@ -43,7 +55,9 @@ export const LibrarySelector = ({ ) : ( - There is no library! + + + )}
); @@ -52,10 +66,13 @@ export const LibrarySelector = ({ export const mapStateToProps = (state) => ({ libraries: selectors.libraries(state), selectedLibrary: selectors.selectedLibrary(state), + settings: selectors.settings(state), }); export const mapDispatchToProps = { + loadLibrary: actions.loadLibrary, onSelectLibrary: actions.onSelectLibrary, + unloadLibrary: actions.unloadLibrary, }; export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LibrarySelector)); diff --git a/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx b/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx index 4566f4159..fa697feb8 100644 --- a/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx +++ b/src/editors/containers/LibraryContentEditor/LibrarySettings.jsx @@ -1,21 +1,20 @@ import React from 'react'; import { connect } from 'react-redux'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Button, Form } from '@edx/paragon'; +import { Form } from '@edx/paragon'; import messages from './messages'; import { modes } from './constants'; import { actions, selectors } from './data'; -import { thunkActions } from '../../data/redux'; export const LibrarySettings = ({ // redux - onCountSettingsChange, - onSelectionModeChange, - onShowResetSettingsChange, + onCountChange, + onModeChange, + onShowResetChange, selectedLibrary, - selectionMode, - selectionSettings, + selectedLibraryId, + settings, }) => { if (selectedLibrary === null) return <>; @@ -23,8 +22,11 @@ export const LibrarySettings = ({
onSelectionModeChange({ selectionMode: e.target.value })} - value={selectionMode} + onChange={e => onModeChange({ + libraryId: selectedLibraryId, + mode: e.target.value, + })} + value={settings[selectedLibraryId]?.mode} > @@ -34,26 +36,17 @@ export const LibrarySettings = ({ - {/* - } - onChange={(e) => onSelectionModeChange({ - selectionMode: (e.target.checked ? modes.selected.value : modes.random.value) - })} - > - - */}
{ - selectionMode === modes.random.value + settings[selectedLibraryId]?.mode === modes.random.value ?
onCountSettingsChange({ + onChange= {(e) => onCountChange({ + libraryId: selectedLibraryId, count: e.target.value, })} - value={selectionSettings.count} + value={settings[selectedLibraryId]?.count} type="number" />