Skip to content

Commit

Permalink
feat: added edit modals for xblocks
Browse files Browse the repository at this point in the history
  • Loading branch information
PKulkoRaccoonGang committed Mar 20, 2024
1 parent 1e02f59 commit 227ab3d
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 79 deletions.
1 change: 0 additions & 1 deletion src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ const CourseUnit = ({ courseId }) => {
handleXBlockDragAndDrop,
canPasteComponent,
} = useCourseUnit({ courseId, blockId });

const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]);
const [unitXBlocks, setUnitXBlocks] = useState(initialXBlocksData);

Expand Down
154 changes: 80 additions & 74 deletions src/course-unit/course-xblock/CourseXBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '../data/selectors';
import {
copyToClipboard,
fetchXBlockModalDataQuery,
fetchXBlockIframeHtmlAndResourcesQuery,
} from '../data/thunk';
import { COMPONENT_TYPES } from '../constants';
Expand All @@ -44,6 +45,7 @@ const CourseXBlock = ({
const intl = useIntl();
const xblockIframeHtmlAndResources = useSelector(getXBlockIframeHtmlAndResources);
const xblockInstanceHtmlAndResources = find(xblockIframeHtmlAndResources, { xblockId: id });
const xblockModalData = useSelector(state => state.courseUnit.xblockModalData);

const visibilityMessage = userPartitionInfo.selectedGroupsLabel
? intl.formatMessage(messages.visibilityMessage, { selectedGroupsLabel: userPartitionInfo.selectedGroupsLabel })
Expand Down Expand Up @@ -73,6 +75,7 @@ const CourseXBlock = ({
navigate(`/course/${courseId}/editor/${type}/${id}`);
break;
default:
dispatch(fetchXBlockModalDataQuery(id));
}
};

Expand All @@ -88,83 +91,86 @@ const CourseXBlock = ({
}, []);

return (
<div ref={courseXBlockElementRef} {...props}>
<Card
as={ConditionalSortableElement}
id={id}
draggable
componentStyle={{ marginBottom: 0 }}
>
<Card.Header
title={title}
subtitle={visibilityMessage}
actions={(
<ActionRow className="mr-2">
<IconButton
alt={intl.formatMessage(messages.blockAltButtonEdit)}
iconAs={EditIcon}
onClick={handleEdit}
/>
<Dropdown>
<Dropdown.Toggle
id={id}
as={IconButton}
src={MoveVertIcon}
alt={intl.formatMessage(messages.blockActionsDropdownAlt)}
iconAs={Icon}
<>
<XBlockContent getHandlerUrl={getHandlerUrl} view={xblockModalData} />
<div ref={courseXBlockElementRef} {...props}>
<Card
as={ConditionalSortableElement}
id={id}
draggable
componentStyle={{ marginBottom: 0 }}
>
<Card.Header
title={title}
subtitle={visibilityMessage}
actions={(
<ActionRow className="mr-2">
<IconButton
alt={intl.formatMessage(messages.blockAltButtonEdit)}
iconAs={EditIcon}
onClick={handleEdit}
/>
<Dropdown.Menu>
<Dropdown.Item onClick={() => unitXBlockActions.handleDuplicate(id)}>
{intl.formatMessage(messages.blockLabelButtonDuplicate)}
</Dropdown.Item>
<Dropdown.Item>
{intl.formatMessage(messages.blockLabelButtonMove)}
</Dropdown.Item>
{canEdit && (
<Dropdown.Item onClick={() => dispatch(copyToClipboard(id))}>
{intl.formatMessage(messages.blockLabelButtonCopyToClipboard)}
<Dropdown>
<Dropdown.Toggle
id={id}
as={IconButton}
src={MoveVertIcon}
alt={intl.formatMessage(messages.blockActionsDropdownAlt)}
iconAs={Icon}
/>
<Dropdown.Menu>
<Dropdown.Item onClick={() => unitXBlockActions.handleDuplicate(id)}>
{intl.formatMessage(messages.blockLabelButtonDuplicate)}
</Dropdown.Item>
)}
<Dropdown.Item onClick={openConfigureModal}>
{intl.formatMessage(messages.blockLabelButtonManageAccess)}
</Dropdown.Item>
<Dropdown.Item onClick={openDeleteModal}>
{intl.formatMessage(messages.blockLabelButtonDelete)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<DeleteModal
category="component"
isOpen={isDeleteModalOpen}
close={closeDeleteModal}
onDeleteSubmit={onDeleteSubmit}
/>
<ConfigureModal
isXBlockComponent
isOpen={isConfigureModalOpen}
onClose={closeConfigureModal}
onConfigureSubmit={onConfigureSubmit}
currentItemData={currentItemData}
/>
</ActionRow>
)}
/>
<Card.Section>
{renderError ? <RenderErrorAlert errorMessage={renderError} /> : (
<>
<XBlockMessages validationMessages={validationMessages} />
{xblockInstanceHtmlAndResources && (
<XBlockContent
getHandlerUrl={getHandlerUrl}
view={xblockInstanceHtmlAndResources}
type={type}
<Dropdown.Item>
{intl.formatMessage(messages.blockLabelButtonMove)}
</Dropdown.Item>
{canEdit && (
<Dropdown.Item onClick={() => dispatch(copyToClipboard(id))}>
{intl.formatMessage(messages.blockLabelButtonCopyToClipboard)}
</Dropdown.Item>
)}
<Dropdown.Item onClick={openConfigureModal}>
{intl.formatMessage(messages.blockLabelButtonManageAccess)}
</Dropdown.Item>
<Dropdown.Item onClick={openDeleteModal}>
{intl.formatMessage(messages.blockLabelButtonDelete)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<DeleteModal
category="component"
isOpen={isDeleteModalOpen}
close={closeDeleteModal}
onDeleteSubmit={onDeleteSubmit}
/>
<ConfigureModal
isXBlockComponent
isOpen={isConfigureModalOpen}
onClose={closeConfigureModal}
onConfigureSubmit={onConfigureSubmit}
currentItemData={currentItemData}
/>
)}
</>
)}
</Card.Section>
</Card>
</div>
</ActionRow>
)}
/>
<Card.Section>
{renderError ? <RenderErrorAlert errorMessage={renderError} /> : (
<>
<XBlockMessages validationMessages={validationMessages} />
{xblockInstanceHtmlAndResources && (
<XBlockContent
getHandlerUrl={getHandlerUrl}
view={xblockInstanceHtmlAndResources}
type={type}
/>
)}
</>
)}
</Card.Section>
</Card>
</div>
</>
);
};

Expand Down
9 changes: 9 additions & 0 deletions src/course-unit/course-xblock/CourseXblock.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,12 @@
margin-bottom: 0;
}
}

.xblock-iframe-modal-wrapper {
height: 100%;
position: absolute;
z-index: 99999999999;
top: 0;
left: 0;
width: 100%;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ensureConfig(['STUDIO_BASE_URL', 'SECURE_ORIGIN_XBLOCK_BOOTSTRAP_HTML_URL'], 'st
* requests as the user. However, it is allowed to call any XBlock handlers.
*/
const XBlockContent = ({
view, type, getHandlerUrl, onBlockNotification,
view, type, getHandlerUrl, onBlockNotification, variant,

Check failure on line 22 in src/course-unit/course-xblock/xblock-content/XBlockContent.jsx

View workflow job for this annotation

GitHub Actions / tests

'variant' is missing in props validation
}) => {
const iframeRef = useRef(null);
const [html, setHtml] = useState(null);
Expand All @@ -35,6 +35,7 @@ const XBlockContent = ({
view.resources,
getConfig().STUDIO_BASE_URL,
type,
variant,
);

// Load the XBlock HTML into the IFrame:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
* JS and CSS dependencies properly.
* @param type The XBlock's type (openassessment, discussion, video, etc.)
*/
export default function wrapBlockHtmlForIFrame(html, sourceResources, studioBaseUrl, type) {
export default function wrapBlockHtmlForIFrame(html, sourceResources, studioBaseUrl, type, variant) {
const resources = normalizeResources(sourceResources);

/* Extract CSS resources. */
Expand All @@ -36,7 +36,7 @@ export default function wrapBlockHtmlForIFrame(html, sourceResources, studioBase
jsTags += scripts.map(script => `<script>${script}</script>`).join('\n');

let legacyIncludes = '';
if (html.indexOf('wrapper-xblock-message') !== -1) {
if (html.indexOf('wrapper-xblock-message') !== -1 || html.indexOf('xmodule_edit') !== -1) {
legacyIncludes += `
<!-- Built-in XBlocks (and some plugins) depends on Studio CSS -->
<!-- At least one XBlock (drag and drop v2) expects Font Awesome -->
Expand Down Expand Up @@ -251,6 +251,53 @@ export default function wrapBlockHtmlForIFrame(html, sourceResources, studioBase
modifiedHtml = modifiedHtml.replace('src=&#34;/static/studio', `src=&#34;${studioBaseUrl}/static/studio`);
modifiedHtml = modifiedHtml.replace('data-target="/preview/xblock', `data-target="${studioBaseUrl}/preview/xblock`);

const hasCustomButtons = html.includes('editor-with-buttons');
const hasCustomTabs = html.includes('editor-with-tabs');
const hasPlugins = html.includes('wrapper-comp-plugins');

Check failure on line 256 in src/course-unit/course-xblock/xblock-content/iframe-wrapper/iframe-wrapper.js

View workflow job for this annotation

GitHub Actions / tests

'hasPlugins' is assigned a value but never used
console.log('TYPE:', type);

Check warning on line 257 in src/course-unit/course-xblock/xblock-content/iframe-wrapper/iframe-wrapper.js

View workflow job for this annotation

GitHub Actions / tests

Unexpected console statement
console.log('VARIANT:', variant);

Check warning on line 258 in src/course-unit/course-xblock/xblock-content/iframe-wrapper/iframe-wrapper.js

View workflow job for this annotation

GitHub Actions / tests

Unexpected console statement
const getDataEditor = html.includes('wrapper-comp-editor');

const editingModalHeader = `
<div class="modal-header">
<h2 id="modal-window-title" class="title modal-window-title">
<span class="modal-button-title">
Editing: displayName
</span>
<button data-tooltip="Edit Title" class="btn-default action-edit title-edit-button">
<span class="icon fa fa-pencil" aria-hidden="true"></span>
<span class="sr"> Edit Title</span>
</button>
</h2>
<ul class="editor-modes action-list action-modes">
<li class="action-item" data-mode="editor">
<a href="#" class="editor-button is-set">Editor</a>
</li>
<li class="action-item" data-mode="settings">
<a href="#" class="settings-button">Settings</a>
</li>
</ul>
</div>`;

const defaultModalTitle = `
<div class="modal-header">
<h2 id="modal-window-title" class="title modal-window-title">Editing: displayName</h2>
<ul class="editor-modes action-list action-modes"></ul>
</div>`;

const getActionBar = `
<div class="modal-actions" style="display: block;">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-save">Save</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>`;

if (
type === COMPONENT_TYPES.discussion
|| type === COMPONENT_TYPES.dragAndDrop
Expand Down Expand Up @@ -328,5 +375,41 @@ export default function wrapBlockHtmlForIFrame(html, sourceResources, studioBase
`;
}

if (variant === 'edit-modal') {
result = `
<!DOCTYPE html>
<html>
<head>
<!-- Open links in a new tab, not this iframe -->
<base target="_blank">
<meta charset="UTF-8">
${legacyIncludes}
${cssTags}
</head>
<!-- A Studio-served stylesheet will set the body min-height to 100% (a common strategy to allow for background
images to fill the viewport), but this has the undesireable side-effect of causing an infinite loop via the
onResize event listeners in certain situations. Resetting it to the default "auto" skirts the problem. -->
<body style="min-height: auto; background-color: #0000006b; overflow: hidden;" class="course container view-container">
<div class="wrapper wrapper-modal-window wrapper-modal-window-edit-xblock">
<div class="modal-window-overlay"></div>
<div class="modal-window modal-editor confirm modal-lg modal-type-discussion" tabindex="-1" aria-labelledby="modal-window-title" style="top: 238.5px; left: 17px;">
<div class="edit-xblock-modal">
${!hasCustomTabs && getDataEditor ? editingModalHeader : defaultModalTitle}
<div class="modal-content">
${html}
</div>
${!hasCustomButtons ? getActionBar : ''}
</div>
</div>
</div>
${jsTags}
<script>
window.addEventListener('load', (${xblockIFrameConnector.toString()}));
</script>
</body>
</html>
`;
}

return result;
}
8 changes: 8 additions & 0 deletions src/course-unit/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
export const getClipboardUrl = () => `${getStudioBaseUrl()}/api/content-staging/v1/clipboard/`;
export const getXBlockContainerPreview = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}/container_preview`;
export const getXBlockModalDataApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}/studio_view`;

export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;

Expand Down Expand Up @@ -199,3 +200,10 @@ export async function getXBlockIframeData(itemId) {

return camelCaseObject(data);
}

export async function getXBlockModalData(itemId) {
const { data } = await getAuthenticatedHttpClient()
.get(getXBlockModalDataApiUrl(itemId));

return camelCaseObject(data);
}
5 changes: 5 additions & 0 deletions src/course-unit/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const slice = createSlice({
clipboardData: null,
staticFileNotices: {},
xblockIframeHtmlAndResources: [],
xblockModalData: [],
},
reducers: {
fetchCourseItemSuccess: (state, { payload }) => {
Expand Down Expand Up @@ -119,6 +120,9 @@ const slice = createSlice({
fetchXBlockIframeResources: (state, { payload }) => {
state.xblockIframeHtmlAndResources.push(payload);
},
fetchXBlockModalData: (state, { payload }) => {
state.xblockModalData = payload;
},
},
});

Expand All @@ -143,6 +147,7 @@ export const {
fetchStaticFileNoticesSuccess,
reorderXBlockList,
fetchXBlockIframeResources,
fetchXBlockModalData,
} = slice.actions;

export const {
Expand Down
Loading

0 comments on commit 227ab3d

Please sign in to comment.