Files
frontend-app-authoring/src/course-unit/course-xblock/CourseXBlock.jsx
Jillian 6b6d3aaa7a Upgrade frontend-build to v14 (#1052)
* fix: warnings about Duplicate message id
* fix: paragon's Hyperlink no longer accepts a 'content' attribute
* test: ensure all act() calls are async
* test: Removed "async" from "describe"
* fix: DiscussionsSettings tests
* Don't nest userAction.click in act() -- nested act() statements have
  indeterminent behaviour.
* Use getBy* instead of findBy* with userAction to avoid nested act() statements
* Always await userEvent.click
* Use fireEvent.click when the onClick handlers need to be called
* Use queryBy* instead of getBy* when using .toBeInTheDocument or 
* fix: typo in data-testid
* test: Use useLocation to test route changes
* Don't nest userAction.click in act() -- nested act() statements have
* chore: fix lint:fix and lint errors
* remove "indent" setting from .eslintrc.js
* add @typescript-eslint/ prefix to eslint-disable-line statements where flagged by linter
* changed stylelint setting import-notation to "string"
* test: fix failing tests after upgrade
* fix: css error "target selector was not found"
* chore: upgrades dependency frontend-lib-content-components@2.3.0
* chore: bumps @edx/frontend-component-ai-translations to ^2.1.0

---------

Co-authored-by: Yusuf Musleh <yusuf@opencraft.com>
2024-06-22 00:14:46 +05:30

193 lines
6.6 KiB
JavaScript

import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import {
ActionRow, Card, Dropdown, Icon, IconButton, useToggle,
} from '@openedx/paragon';
import { EditOutline as EditIcon, MoreVert as MoveVertIcon } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { getCanEdit, getCourseId } from 'CourseAuthoring/course-unit/data/selectors';
import DeleteModal from '../../generic/delete-modal/DeleteModal';
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import SortableItem from '../../generic/drag-helper/SortableItem';
import { scrollToElement } from '../../course-outline/utils';
import { COURSE_BLOCK_NAMES } from '../../constants';
import { copyToClipboard } from '../../generic/data/thunks';
import { COMPONENT_TYPES } from '../constants';
import XBlockMessages from './xblock-messages/XBlockMessages';
import messages from './messages';
const CourseXBlock = ({
id, title, type, unitXBlockActions, shouldScroll, userPartitionInfo,
handleConfigureSubmit, validationMessages, ...props
}) => {
const courseXBlockElementRef = useRef(null);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
const dispatch = useDispatch();
const navigate = useNavigate();
const canEdit = useSelector(getCanEdit);
const courseId = useSelector(getCourseId);
const intl = useIntl();
const [searchParams] = useSearchParams();
const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === id;
const visibilityMessage = userPartitionInfo.selectedGroupsLabel
? intl.formatMessage(messages.visibilityMessage, { selectedGroupsLabel: userPartitionInfo.selectedGroupsLabel })
: null;
const currentItemData = {
category: COURSE_BLOCK_NAMES.component.id,
displayName: title,
userPartitionInfo,
showCorrectness: 'always',
};
const onDeleteSubmit = () => {
unitXBlockActions.handleDelete(id);
closeDeleteModal();
};
const handleEdit = () => {
switch (type) {
case COMPONENT_TYPES.html:
case COMPONENT_TYPES.problem:
case COMPONENT_TYPES.video:
navigate(`/course/${courseId}/editor/${type}/${id}`);
break;
default:
}
};
const onConfigureSubmit = (...arg) => {
handleConfigureSubmit(id, ...arg, closeConfigureModal);
};
useEffect(() => {
// if this item has been newly added, scroll to it.
if (courseXBlockElementRef.current && (shouldScroll || isScrolledToElement)) {
scrollToElement(courseXBlockElementRef.current);
}
}, [isScrolledToElement]);
return (
<div
ref={courseXBlockElementRef}
{...props}
className={classNames('course-unit__xblock', {
'xblock-highlight': isScrolledToElement,
})}
>
<Card
as={SortableItem}
id={id}
draggable
category="xblock"
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}
/>
<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.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>
<XBlockMessages validationMessages={validationMessages} />
<div className="w-100 bg-gray-100" style={{ height: 200 }} data-block-id={id} />
</Card.Section>
</Card>
</div>
);
};
CourseXBlock.defaultProps = {
validationMessages: [],
shouldScroll: false,
};
CourseXBlock.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
shouldScroll: PropTypes.bool,
validationMessages: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.string,
text: PropTypes.string,
})),
unitXBlockActions: PropTypes.shape({
handleDelete: PropTypes.func,
handleDuplicate: PropTypes.func,
}).isRequired,
userPartitionInfo: PropTypes.shape({
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
groups: PropTypes.arrayOf(PropTypes.shape({
deleted: PropTypes.bool,
id: PropTypes.number,
name: PropTypes.string,
selected: PropTypes.bool,
})),
id: PropTypes.number,
name: PropTypes.string,
scheme: PropTypes.string,
})),
selectedPartitionIndex: PropTypes.number,
selectedGroupsLabel: PropTypes.string,
}).isRequired,
handleConfigureSubmit: PropTypes.func.isRequired,
};
export default CourseXBlock;