Skip to content
This repository has been archived by the owner on Jul 18, 2024. It is now read-only.

Commit

Permalink
refactor: Split LibraryAuthoringPage to components
Browse files Browse the repository at this point in the history
  • Loading branch information
yusuf-musleh committed Feb 12, 2024
1 parent 8c4f967 commit a5e0748
Show file tree
Hide file tree
Showing 6 changed files with 512 additions and 441 deletions.
193 changes: 193 additions & 0 deletions src/library-authoring/author-library/BlockPreviewBase.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
ActionRow,
Button,
IconButton,
Card,
Dropdown,
ModalDialog,
Icon,
IconButtonWithTooltip,
OverlayTrigger,
Tooltip,
} from '@edx/paragon';
import {
EditOutline,
MoreVert,
Tag,
} from '@edx/paragon/icons';
import { EditorPage } from '@edx/frontend-lib-content-components';
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { LibraryBlock } from '../edit-block/LibraryBlock';
import {
getXBlockHandlerUrl,
libraryBlockShape,
libraryShape,
fetchable,
XBLOCK_VIEW_SYSTEM,
} from '../common';
import messages from './messages';
import { blockViewShape } from '../edit-block/data/shapes';

ensureConfig(['STUDIO_BASE_URL'], 'library API service');

const getHandlerUrl = async (blockId) => getXBlockHandlerUrl(blockId, XBLOCK_VIEW_SYSTEM.Studio, 'handler_name');

/**
* BlockPreviewBase
* Template component for BlockPreview cards, which are used to display
* components and render controls for them in a library listing.
*/
export const BlockPreviewBase = ({
intl, block, view, canEdit, showPreviews, showDeleteModal,
setShowDeleteModal, showEditorModal, setShowEditorModal, setOpenContentTagsDrawer,
library, editView, isLtiUrlGenerating,
...props
}) => (
<Card className="w-auto my-3">
<Card.Header
className="library-authoring-block-card-header"
title={block.display_name}
actions={(
<ActionRow>
{
!!block.tags_count && (
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="manage-tags-tooltip">{intl.formatMessage(messages['library.detail.block.manage_tags'])}</Tooltip>
}
>
<Button
variant="outline-primary"
iconBefore={Tag}
className="tags-count-manage-button"
onClick={() => setOpenContentTagsDrawer(block.id)}
data-testid="tags-count-manage-tags-button"
>
{ block.tags_count }
</Button>
</OverlayTrigger>
)
}
<IconButtonWithTooltip
aria-label={intl.formatMessage(messages['library.detail.block.edit'])}
onClick={() => setShowEditorModal(true)}
src={EditOutline}
iconAs={Icon}
tooltipContent={intl.formatMessage(messages['library.detail.block.edit'])}
/>
<OverlayTrigger
placement="top"
overlay={(
<Tooltip id="more-actions-tooltip">
{intl.formatMessage(messages['library.detail.block.more_actions'])}
</Tooltip>
)}
>
<Dropdown>
<Dropdown.Toggle
aria-label={intl.formatMessage(messages['library.detail.block.more_actions'])}
as={IconButton}
src={MoreVert}
iconAs={Icon}
/>
<Dropdown.Menu align="right">
<Dropdown.Item
aria-label={intl.formatMessage(messages['library.detail.block.manage_tags'])}
onClick={() => setOpenContentTagsDrawer(block.id)}
>
{intl.formatMessage(messages['library.detail.block.manage_tags'])}
</Dropdown.Item>
<Dropdown.Item
aria-label={intl.formatMessage(messages['library.detail.block.delete'])}
onClick={() => setShowDeleteModal(true)}
>
{intl.formatMessage(messages['library.detail.block.delete'])}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</OverlayTrigger>
</ActionRow>
)}
/>
<ModalDialog
isOpen={showEditorModal}
hasCloseButton={false}
size="fullscreen"
>
<EditorPage
blockType={block.block_type}
blockId={block.id}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
returnFunction={() => (response) => {
setShowEditorModal(false);
if (response && response.metadata) {
props.setLibraryBlockDisplayName({

Check warning on line 129 in src/library-authoring/author-library/BlockPreviewBase.jsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/author-library/BlockPreviewBase.jsx#L129

Added line #L129 was not covered by tests
blockId: block.id,
displayName: response.metadata.display_name,
});
// This state change triggers the iframe to reload.
props.updateLibraryBlockView({ blockId: block.id });

Check warning on line 134 in src/library-authoring/author-library/BlockPreviewBase.jsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/author-library/BlockPreviewBase.jsx#L134

Added line #L134 was not covered by tests
}
}}
/>
</ModalDialog>
<ModalDialog
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}

Check warning on line 141 in src/library-authoring/author-library/BlockPreviewBase.jsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/author-library/BlockPreviewBase.jsx#L141

Added line #L141 was not covered by tests
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages['library.detail.block.delete.modal.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{intl.formatMessage(messages['library.detail.block.delete.modal.body'])}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages['library.detail.block.delete.modal.cancel.button'])}
</ModalDialog.CloseButton>
<Button onClick={() => props.deleteLibraryBlock({ blockId: block.id })} variant="primary">
{intl.formatMessage(messages['library.detail.block.delete.modal.confirmation.button'])}
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
{showPreviews && (
<Card.Body>
<LibraryBlock getHandlerUrl={getHandlerUrl} view={view} />
</Card.Body>
)}
</Card>
);

BlockPreviewBase.propTypes = {
block: libraryBlockShape.isRequired,
canEdit: PropTypes.bool.isRequired,
deleteLibraryBlock: PropTypes.func.isRequired,
editView: PropTypes.string.isRequired,
intl: intlShape.isRequired,
isLtiUrlGenerating: PropTypes.bool,
library: libraryShape.isRequired,
setLibraryBlockDisplayName: PropTypes.func.isRequired,
setShowDeleteModal: PropTypes.func.isRequired,
setShowEditorModal: PropTypes.func.isRequired,
showDeleteModal: PropTypes.bool.isRequired,
showEditorModal: PropTypes.bool.isRequired,
showPreviews: PropTypes.bool.isRequired,
setOpenContentTagsDrawer: PropTypes.func.isRequired,
updateLibraryBlockView: PropTypes.bool.isRequired,
view: fetchable(blockViewShape).isRequired,
};

BlockPreviewBase.defaultProps = {
isLtiUrlGenerating: false,
};

export const BlockPreview = injectIntl(BlockPreviewBase);
129 changes: 129 additions & 0 deletions src/library-authoring/author-library/BlockPreviewContainer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { intlShape } from '@edx/frontend-platform/i18n';
import { BlockPreview } from './BlockPreviewBase';
import {
BLOCK_TYPE_EDIT_DENYLIST,
libraryBlockShape,
libraryShape,
LOADING_STATUS,
ROUTES,
XBLOCK_VIEW_SYSTEM,
fetchable,
} from '../common';
import { LoadingPage } from '../../generic';
import messages from './messages';
import { blockStatesShape } from '../edit-block/data/shapes';

ensureConfig(['STUDIO_BASE_URL'], 'library API service');

const inStandby = ({ blockStates, id, attr }) => blockStates[id][attr].status === LOADING_STATUS.STANDBY;
const needsView = ({ blockStates, id }) => inStandby({ blockStates, id, attr: 'view' });
const needsMeta = ({ blockStates, id }) => inStandby({ blockStates, id, attr: 'metadata' });

/**
* BlockPreviewContainerBase
* Container component for the BlockPreview cards.
* Handles the fetching of the block view and metadata.
*/
export const BlockPreviewContainerBase = ({
intl, block, blockView, blockStates, showPreviews, setOpenContentTagsDrawer, library, ltiUrlClipboard, ...props
}) => {
// There are enough events that trigger the effects here that we need to keep track of what we're doing to avoid
// doing it more than once, or running them when the state can no longer support these actions.
//
// This problem feels like there should be some way to generalize it and wrap it to avoid this issue.
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showEditorModal, setShowEditorModal] = useState(false);

useEffect(() => {
props.initializeBlock({
blockId: block.id,
});
}, []);
useEffect(() => {
if (!blockStates[block.id] || !showPreviews) {
return;
}
if (needsMeta({ blockStates, id: block.id })) {
props.fetchLibraryBlockMetadata({ blockId: block.id });
}
if (needsView({ blockStates, id: block.id })) {
props.fetchLibraryBlockView({
blockId: block.id,
viewSystem: XBLOCK_VIEW_SYSTEM.Studio,
viewName: 'student_view',
});
}
}, [blockStates[block.id], showPreviews]);

if (blockStates[block.id] === undefined) {
return <LoadingPage loadingMessage={intl.formatMessage(messages['library.detail.loading.message'])} />;
}
const { metadata } = blockStates[block.id];
const canEdit = metadata !== null && !BLOCK_TYPE_EDIT_DENYLIST.includes(metadata.block_type);

let editView;
if (canEdit) {
editView = ROUTES.Block.EDIT_SLUG(library.id, block.id);
} else {
editView = ROUTES.Detail.HOME_SLUG(library.id, block.id);

Check warning on line 72 in src/library-authoring/author-library/BlockPreviewContainer.jsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/author-library/BlockPreviewContainer.jsx#L71-L72

Added lines #L71 - L72 were not covered by tests
}

let isLtiUrlGenerating;
if (library.allow_lti) {
const isBlockOnClipboard = ltiUrlClipboard.value.blockId === block.id;

Check warning on line 77 in src/library-authoring/author-library/BlockPreviewContainer.jsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/author-library/BlockPreviewContainer.jsx#L77

Added line #L77 was not covered by tests
isLtiUrlGenerating = isBlockOnClipboard && ltiUrlClipboard.status === LOADING_STATUS.LOADING;

if (isBlockOnClipboard && ltiUrlClipboard.status === LOADING_STATUS.LOADED) {
const clipboard = document.createElement('textarea');
clipboard.value = getConfig().STUDIO_BASE_URL + ltiUrlClipboard.value.lti_url;
document.body.appendChild(clipboard);
clipboard.select();
document.execCommand('copy');
document.body.removeChild(clipboard);

Check warning on line 86 in src/library-authoring/author-library/BlockPreviewContainer.jsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/author-library/BlockPreviewContainer.jsx#L81-L86

Added lines #L81 - L86 were not covered by tests
}
}

return (
<BlockPreview
block={block}
canEdit={canEdit}
editView={editView}
isLtiUrlGenerating={isLtiUrlGenerating}
library={library}
setShowDeleteModal={setShowDeleteModal}
setShowEditorModal={setShowEditorModal}
showDeleteModal={showDeleteModal}
showEditorModal={showEditorModal}
showPreviews={showPreviews}
setOpenContentTagsDrawer={setOpenContentTagsDrawer}
view={blockView(block)}
{...props}
/>
);
};

BlockPreviewContainerBase.defaultProps = {
blockView: null,
ltiUrlClipboard: null,
};

BlockPreviewContainerBase.propTypes = {
block: libraryBlockShape.isRequired,
blockStates: blockStatesShape.isRequired,
blockView: PropTypes.func,
fetchLibraryBlockView: PropTypes.func.isRequired,
fetchLibraryBlockMetadata: PropTypes.func.isRequired,
initializeBlock: PropTypes.func.isRequired,
intl: intlShape.isRequired,
library: libraryShape.isRequired,
// eslint-disable-next-line react/forbid-prop-types
ltiUrlClipboard: fetchable(PropTypes.object),
showPreviews: PropTypes.bool.isRequired,
setOpenContentTagsDrawer: PropTypes.func.isRequired,
};

export const BlockPreviewContainer = BlockPreviewContainerBase;
44 changes: 44 additions & 0 deletions src/library-authoring/author-library/ButtonTogglesBase.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';

import messages from './messages';

const ButtonTogglesBase = ({ setShowPreviews, showPreviews, intl }) => (
<>
<Button
variant="outline-primary"
className="ml-1"
onClick={() => setShowPreviews(!showPreviews)}
size="sm"
>
{ intl.formatMessage(showPreviews ? messages['library.detail.hide_previews'] : messages['library.detail.show_previews']) }
</Button>
{/* todo: either replace the scroll to the add components button functionality
with a better UX for the add component button at the top, or just
remove it entirely */}
<Button
variant="primary"
className="mr-1"
size="sm"
onClick={() => {
const addComponentSection = document.getElementById('add-component-section');
addComponentSection.scrollIntoView({ behavior: 'smooth' });
}}
iconBefore={Add}
>
{intl.formatMessage(messages['library.detail.add.new.component.item'])}
</Button>
</>
);

ButtonTogglesBase.propTypes = {
intl: intlShape.isRequired,
showPreviews: PropTypes.bool.isRequired,
setShowPreviews: PropTypes.func.isRequired,
};

const ButtonToggles = injectIntl(ButtonTogglesBase);
export default ButtonToggles;
Loading

0 comments on commit a5e0748

Please sign in to comment.