Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Library Info Sidebar #46

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/course-unit/add-component/AddComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle } from '@openedx/paragon';

import { getCourseSectionVertical } from '../data/selectors';
import { COMPONENT_TYPES } from '../constants';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn';
import messages from './messages';
Expand Down
2 changes: 1 addition & 1 deletion src/course-unit/add-component/AddComponent.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { executeThunk } from '../../utils';
import { fetchCourseSectionVerticalData } from '../data/thunk';
import { getCourseSectionVerticalApiUrl } from '../data/api';
import { courseSectionVerticalMock } from '../__mocks__';
import { COMPONENT_TYPES } from '../constants';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import AddComponent from './AddComponent';
import messages from './messages';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { Icon } from '@openedx/paragon';
import { EditNote as EditNoteIcon } from '@openedx/paragon/icons';

import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../constants';
import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';

const AddComponentIcon = ({ type }) => {
const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon;
Expand Down
47 changes: 0 additions & 47 deletions src/course-unit/constants.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,6 @@
import {
BackHand as BackHandIcon,
BookOpen as BookOpenIcon,
Edit as EditIcon,
EditNote as EditNoteIcon,
FormatListBulleted as FormatListBulletedIcon,
HelpOutline as HelpOutlineIcon,
LibraryAdd as LibraryIcon,
Lock as LockIcon,
QuestionAnswerOutline as QuestionAnswerOutlineIcon,
Science as ScienceIcon,
TextFields as TextFieldsIcon,
VideoCamera as VideoCameraIcon,
} from '@openedx/paragon/icons';

import messages from './sidebar/messages';
import addComponentMessages from './add-component/messages';

export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock'];

export const COMPONENT_TYPES = {
advanced: 'advanced',
discussion: 'discussion',
library: 'library',
html: 'html',
openassessment: 'openassessment',
problem: 'problem',
video: 'video',
dragAndDrop: 'drag-and-drop-v2',
};

export const TYPE_ICONS_MAP = {
video: VideoCameraIcon,
other: BookOpenIcon,
vertical: FormatListBulletedIcon,
problem: EditIcon,
lock: LockIcon,
};

export const COMPONENT_TYPE_ICON_MAP = {
[COMPONENT_TYPES.advanced]: ScienceIcon,
[COMPONENT_TYPES.discussion]: QuestionAnswerOutlineIcon,
[COMPONENT_TYPES.library]: LibraryIcon,
[COMPONENT_TYPES.html]: TextFieldsIcon,
[COMPONENT_TYPES.openassessment]: EditNoteIcon,
[COMPONENT_TYPES.problem]: HelpOutlineIcon,
[COMPONENT_TYPES.video]: VideoCameraIcon,
[COMPONENT_TYPES.dragAndDrop]: BackHandIcon,
};

export const getUnitReleaseStatus = (intl) => ({
release: intl.formatMessage(messages.releaseStatusTitle),
released: intl.formatMessage(messages.releasedStatusTitle),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import PropTypes from 'prop-types';
import { Icon } from '@openedx/paragon';
import { BookOpen as BookOpenIcon } from '@openedx/paragon/icons';

import { TYPE_ICONS_MAP, UNIT_ICON_TYPES } from '../../constants';
import { UNIT_TYPE_ICONS_MAP, UNIT_ICON_TYPES } from '../../../generic/block-type-utils/constants';

const UnitIcon = ({ type }) => {
const icon = TYPE_ICONS_MAP[type] || BookOpenIcon;
const icon = UNIT_TYPE_ICONS_MAP[type] || BookOpenIcon;

return <Icon src={icon} screenReaderText={type} />;
};
Expand Down
2 changes: 1 addition & 1 deletion src/course-unit/course-xblock/CourseXBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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 { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import XBlockMessages from './xblock-messages/XBlockMessages';
import messages from './messages';

Expand Down
3 changes: 2 additions & 1 deletion src/course-unit/course-xblock/CourseXBlock.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { getCourseSectionVerticalApiUrl, getXBlockBaseApiUrl } from '../data/api
import { fetchCourseSectionVerticalData } from '../data/thunk';
import { executeThunk } from '../../utils';
import { getCourseId } from '../data/selectors';
import { PUBLISH_TYPES, COMPONENT_TYPES } from '../constants';
import { PUBLISH_TYPES } from '../constants';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import { courseSectionVerticalMock, courseVerticalChildrenMock } from '../__mocks__';
import CourseXBlock from './CourseXBlock';
import messages from './messages';
Expand Down
69 changes: 69 additions & 0 deletions src/generic/block-type-utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import {
BackHand as BackHandIcon,
BookOpen as BookOpenIcon,
Edit as EditIcon,
EditNote as EditNoteIcon,
FormatListBulleted as FormatListBulletedIcon,
HelpOutline as HelpOutlineIcon,
LibraryAdd as LibraryIcon,
Lock as LockIcon,
QuestionAnswerOutline as QuestionAnswerOutlineIcon,
Science as ScienceIcon,
TextFields as TextFieldsIcon,
VideoCamera as VideoCameraIcon,
Folder,
} from '@openedx/paragon/icons';

export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock'];

export const COMPONENT_TYPES = {
advanced: 'advanced',
discussion: 'discussion',
library: 'library',
html: 'html',
openassessment: 'openassessment',
problem: 'problem',
video: 'video',
dragAndDrop: 'drag-and-drop-v2',
};

export const UNIT_TYPE_ICONS_MAP: Record<string, React.ComponentType> = {
video: VideoCameraIcon,
other: BookOpenIcon,
vertical: FormatListBulletedIcon,
problem: EditIcon,
lock: LockIcon,
};

export const COMPONENT_TYPE_ICON_MAP: Record<string, React.ComponentType> = {
[COMPONENT_TYPES.advanced]: ScienceIcon,
[COMPONENT_TYPES.discussion]: QuestionAnswerOutlineIcon,
[COMPONENT_TYPES.library]: LibraryIcon,
[COMPONENT_TYPES.html]: TextFieldsIcon,
[COMPONENT_TYPES.openassessment]: EditNoteIcon,
[COMPONENT_TYPES.problem]: HelpOutlineIcon,
[COMPONENT_TYPES.video]: VideoCameraIcon,
[COMPONENT_TYPES.dragAndDrop]: BackHandIcon,
};

export const STRUCTURAL_TYPE_ICONS: Record<string, React.ComponentType> = {
vertical: UNIT_TYPE_ICONS_MAP.vertical,
sequential: Folder,
chapter: Folder,
};

export const COMPONENT_TYPE_STYLE_COLOR_MAP = {
[COMPONENT_TYPES.advanced]: 'component-style-other',
[COMPONENT_TYPES.discussion]: 'component-style-default',
[COMPONENT_TYPES.library]: 'component-style-default',
[COMPONENT_TYPES.html]: 'component-style-html',
[COMPONENT_TYPES.openassessment]: 'component-style-default',
[COMPONENT_TYPES.problem]: 'component-style-default',
[COMPONENT_TYPES.video]: 'component-style-video',
[COMPONENT_TYPES.dragAndDrop]: 'component-style-default',
vertical: 'component-style-vertical',
sequential: 'component-style-default',
chapter: 'component-style-default',
collection: 'component-style-collection',
};
47 changes: 47 additions & 0 deletions src/generic/block-type-utils/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.component-style-default {
background-color: #005C9E;

.pgn__icon {
color: white;
}
}

.component-style-html {
background-color: #9747FF;

.pgn__icon {
color: white;
}
}

.component-style-collection {
background-color: #FFCD29;

.pgn__icon {
color: black;
}
}

.component-style-video {
background-color: #358F0A;

.pgn__icon {
color: white;
}
}

.component-style-vertical {
background-color: #0B8E77;

.pgn__icon {
color: white;
}
}

.component-style-other {
background-color: #646464;

.pgn__icon {
color: white;
}
}
15 changes: 15 additions & 0 deletions src/generic/block-type-utils/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { Article } from '@openedx/paragon/icons';
import {
COMPONENT_TYPE_ICON_MAP,
STRUCTURAL_TYPE_ICONS,
COMPONENT_TYPE_STYLE_COLOR_MAP,
} from './constants';

export function getItemIcon(blockType: string): React.ComponentType {
return STRUCTURAL_TYPE_ICONS[blockType] ?? COMPONENT_TYPE_ICON_MAP[blockType] ?? Article;
}

export function getComponentStyleColor(blockType: string): string {
return COMPONENT_TYPE_STYLE_COLOR_MAP[blockType] ?? 'bg-component';
}
1 change: 1 addition & 0 deletions src/generic/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
@import "./modal-dropzone/ModalDropzone";
@import "./configure-modal/ConfigureModal";
@import "./drag-helper/SortableItem";
@import "./block-type-utils";
69 changes: 69 additions & 0 deletions src/generic/toast-context/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { ToastContext, ToastProvider } from '.';
import initializeStore from '../../store';

export interface WraperProps {
children: React.ReactNode;
}

const TestComponentToShow = () => {
const { showToast } = React.useContext(ToastContext);

React.useEffect(() => {
showToast('This is the toast!');
}, [showToast]);

return <div>Content</div>;
};

const TestComponentToClose = () => {
const { showToast, closeToast } = React.useContext(ToastContext);

React.useEffect(() => {
showToast('This is the toast!');
closeToast();
}, [showToast]);

return <div>Content</div>;
};

let store;
const RootWrapper = ({ children }: WraperProps) => (
<AppProvider store={store}>
<ToastProvider>
{children}
</ToastProvider>
</AppProvider>
);

describe('<ToastProvider />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});

afterEach(() => {
jest.clearAllMocks();
});

it('should show toast', async () => {
render(<RootWrapper><TestComponentToShow /></RootWrapper>);
expect(await screen.findByText('This is the toast!')).toBeInTheDocument();
});

it('should close toast', async () => {
render(<RootWrapper><TestComponentToClose /></RootWrapper>);
expect(await screen.findByText('Content')).toBeInTheDocument();
expect(screen.queryByText('This is the toast!')).not.toBeInTheDocument();
});
});
57 changes: 57 additions & 0 deletions src/generic/toast-context/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { Toast } from '@openedx/paragon';

export interface ToastContextData {
toastMessage: string | null;
showToast: (message: string) => void;
closeToast: () => void;
}

export interface ToastProviderProps {
children: React.ReactNode;
}

/**
* Global context to keep track of popup message(s) that appears to user after
* they take an action like creating or deleting something.
*/
export const ToastContext = React.createContext({
toastMessage: null,
showToast: () => {},
closeToast: () => {},
} as ToastContextData);

/**
* React component to provide `ToastContext` to the app
*/
export const ToastProvider = (props: ToastProviderProps) => {
// TODO, We can convert this to a queue of messages,
// see: https://github.com/open-craft/frontend-app-course-authoring/pull/38#discussion_r1638990647

const [toastMessage, setToastMessage] = React.useState<string | null>(null);

React.useEffect(() => () => {
// Cleanup function to avoid updating state on unmounted component
setToastMessage(null);
}, []);

const showToast = React.useCallback((message) => setToastMessage(message), [setToastMessage]);
const closeToast = React.useCallback(() => setToastMessage(null), [setToastMessage]);

const context = React.useMemo(() => ({
toastMessage,
showToast,
closeToast,
}), [toastMessage, showToast, closeToast]);

return (
<ToastContext.Provider value={context}>
{props.children}
{ toastMessage && (
<Toast show={toastMessage !== null} onClose={closeToast}>
{toastMessage}
</Toast>
)}
</ToastContext.Provider>
);
};
Loading