{children}
{actions.childAddable && (
-
+ <>
+
+ {enableCopyPasteUnits && (
+
+ )}
+ >
)}
)}
@@ -214,6 +228,7 @@ SubsectionCard.propTypes = {
hasChanges: PropTypes.bool.isRequired,
visibilityState: PropTypes.string.isRequired,
shouldScroll: PropTypes.bool,
+ enableCopyPasteUnits: PropTypes.bool,
actions: PropTypes.shape({
deletable: PropTypes.bool.isRequired,
draggable: PropTypes.bool.isRequired,
@@ -235,6 +250,7 @@ SubsectionCard.propTypes = {
canMoveItem: PropTypes.func.isRequired,
onOrderChange: PropTypes.func.isRequired,
onOpenConfigureModal: PropTypes.func.isRequired,
+ onPasteClick: PropTypes.func.isRequired,
};
export default SubsectionCard;
diff --git a/src/course-outline/subsection-card/messages.js b/src/course-outline/subsection-card/messages.js
index 90ca407b1b..b4a0b5661a 100644
--- a/src/course-outline/subsection-card/messages.js
+++ b/src/course-outline/subsection-card/messages.js
@@ -5,6 +5,10 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.subsection.button.new-unit',
defaultMessage: 'New unit',
},
+ pasteButton: {
+ id: 'course-authoring.course-outline.subsection.button.new-unit',
+ defaultMessage: 'Paste unit',
+ },
});
export default messages;
diff --git a/src/course-outline/unit-card/UnitCard.jsx b/src/course-outline/unit-card/UnitCard.jsx
index 1601b54e4f..1cb95f718a 100644
--- a/src/course-outline/unit-card/UnitCard.jsx
+++ b/src/course-outline/unit-card/UnitCard.jsx
@@ -28,6 +28,7 @@ const UnitCard = ({
onDuplicateSubmit,
getTitleLink,
onOrderChange,
+ onCopyToClipboardClick,
}) => {
const currentRef = useRef(null);
const dispatch = useDispatch();
@@ -42,6 +43,7 @@ const UnitCard = ({
visibilityState,
actions: unitActions,
isHeaderVisible = true,
+ enableCopyPasteUnits = false,
} = unit;
// re-create actions object for customizations
@@ -80,6 +82,10 @@ const UnitCard = ({
onOrderChange(index, index + 1);
};
+ const handleCopyClick = () => {
+ onCopyToClipboardClick(unit.id);
+ };
+
const titleComponent = (
', () => {
expect(within(element).queryByTestId('unit-card-header__menu-duplicate-button')).not.toBeInTheDocument();
expect(within(element).queryByTestId('unit-card-header__menu-delete-button')).not.toBeInTheDocument();
});
+
+ it('shows copy option based on enableCopyPasteUnits flag', async () => {
+ const { findByTestId } = renderComponent({
+ unit: {
+ ...unit,
+ enableCopyPasteUnits: true,
+ },
+ });
+ const element = await findByTestId('unit-card');
+ const menu = await within(element).findByTestId('unit-card-header__menu-button');
+ await act(async () => fireEvent.click(menu));
+ expect(within(element).queryByText(cardMessages.menuCopy.defaultMessage)).toBeInTheDocument();
+ });
});
diff --git a/src/generic/broadcast-channel/hooks.js b/src/generic/broadcast-channel/hooks.js
new file mode 100644
index 0000000000..230c153d18
--- /dev/null
+++ b/src/generic/broadcast-channel/hooks.js
@@ -0,0 +1,46 @@
+import {
+ useCallback, useEffect, useMemo, useRef,
+} from 'react';
+import { BroadcastChannel } from 'broadcast-channel';
+
+const channelInstances = {};
+
+export const getSingletonChannel = (name) => {
+ if (!channelInstances[name]) {
+ channelInstances[name] = new BroadcastChannel(name);
+ }
+ return channelInstances[name];
+};
+
+export const useBroadcastChannel = (channelName, onMessageReceived) => {
+ const channel = useMemo(() => getSingletonChannel(channelName), [channelName]);
+ const isSubscribed = useRef(false);
+
+ useEffect(() => {
+ if (!isSubscribed.current || process.env.NODE_ENV !== 'development') {
+ // BroadcastChannel api from npm has minor difference from native BroadcastChannel
+ // Native BroadcastChannel passes event to onmessage callback and to
+ // access data we need to use `event.data`, but npm BroadcastChannel
+ // directly passes data as seen below
+ channel.onmessage = (data) => onMessageReceived(data);
+ }
+ return () => {
+ if (isSubscribed.current || process.env.NODE_ENV !== 'development') {
+ channel.close();
+ isSubscribed.current = true;
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const postMessage = useCallback(
+ (message) => {
+ channel?.postMessage(message);
+ },
+ [channel],
+ );
+
+ return {
+ postMessage,
+ };
+};