Skip to content

Display Container Publish status and confirm before publish #2186

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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
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
16 changes: 5 additions & 11 deletions src/generic/Loading.jsx → src/generic/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Spinner } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

export const LoadingSpinner = ({ size }) => (
interface LoadingSpinnerProps {
size?: string;
}

export const LoadingSpinner = ({ size }: LoadingSpinnerProps) => (
<Spinner
animation="border"
role="status"
Expand All @@ -19,14 +21,6 @@ export const LoadingSpinner = ({ size }) => (
/>
);

LoadingSpinner.defaultProps = {
size: undefined,
};

LoadingSpinner.propTypes = {
size: PropTypes.string,
};

const Loading = () => (
<div className="d-flex justify-content-center align-items-center flex-column vh-100">
<LoadingSpinner />
Expand Down
3 changes: 3 additions & 0 deletions src/generic/block-type-utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Folder,
ViewCarousel,
ViewDay,
Widgets,
WidthWide,
} from '@openedx/paragon/icons';
import NewsstandIcon from '../NewsstandIcon';
Expand Down Expand Up @@ -43,6 +44,7 @@ export const UNIT_TYPE_ICONS_MAP: Record<string, React.ComponentType> = {
chapter: ViewCarousel,
problem: EditIcon,
lock: LockIcon,
multiple: Widgets,
};

export const COMPONENT_TYPE_ICON_MAP: Record<string, React.ComponentType> = {
Expand All @@ -65,6 +67,7 @@ export const STRUCTURAL_TYPE_ICONS: Record<string, React.ComponentType> = {
subsection: UNIT_TYPE_ICONS_MAP.sequential,
chapter: UNIT_TYPE_ICONS_MAP.chapter,
section: UNIT_TYPE_ICONS_MAP.chapter,
components: UNIT_TYPE_ICONS_MAP.multiple,
collection: Folder,
libraryContent: Folder,
paste: ContentPasteIcon,
Expand Down
5 changes: 5 additions & 0 deletions src/generic/key-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,9 @@ export enum ContainerType {
Chapter = 'chapter',
Sequential = 'sequential',
Vertical = 'vertical',
/**
* Components are not strictly a container type, but we add this here for simplicity when rendering the container
* hierarchy.
*/
Components = 'components',
}
54 changes: 54 additions & 0 deletions src/library-authoring/containers/ContainerHierarchy.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.content-hierarchy {
margin-bottom: var(--pgn-spacing-paragraph-margin-bottom);

.hierarchy-row {
border: 1px solid var(--pgn-color-light-500);
border-radius: 4px;
background-color: var(--pgn-color-white);
padding: 0;
margin: 0;

&.selected {
border: 3px solid var(--pgn-color-primary-500);
border-radius: 4px;
}

.icon {
background-color: var(--pgn-color-light-300);
border-top: 2px solid var(--pgn-color-light-300);
border-bottom: 2px solid var(--pgn-color-light-300);
border-right: 1px solid var(--pgn-color-light-500);
border-radius: 1px 0 0 1px;
padding: 8px 12px;
}

&.selected .icon {
background-color: var(--pgn-color-primary-500);
border-color: var(--pgn-color-primary-500);
color: var(--pgn-color-white);
}

.text {
padding: 8px 12px;
flex-grow: 2;
}

.publish-status {
background-color: var(--pgn-color-info-200);
white-space: nowrap;
padding: 8px 12px;
}
}

.hierarchy-arrow {
color: var(--pgn-color-light-500);
padding: 0 0 0 14px;
position: relative;
top: -4px;
height: 20px;

&.selected {
color: var(--pgn-color-primary-500);
}
}
}
211 changes: 211 additions & 0 deletions src/library-authoring/containers/ContainerHierarchy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import type { MessageDescriptor } from 'react-intl';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Container, Icon, Stack } from '@openedx/paragon';
import { ArrowDownward, Check, Description } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { getItemIcon } from '@src/generic/block-type-utils';
import Loading from '@src/generic/Loading';
import { ContainerType } from '@src/generic/key-utils';
import type { ContainerHierarchyMember } from '../data/api';
import { useContainerHierarchy } from '../data/apiHooks';
import { useSidebarContext } from '../common/context/SidebarContext';
import messages from './messages';

const ContainerHierarchyRow = ({
containerType,
text,
selected,
showArrow,
willPublish = false,
publishMessage = undefined,
}: {
containerType: ContainerType,
text: string,
selected: boolean,
showArrow: boolean,
willPublish?: boolean,
publishMessage?: MessageDescriptor,
}) => (
<Stack>
<Container
className={classNames('hierarchy-row', { selected })}
>
<Stack
direction="horizontal"
gap={2}
>
<div className="icon">
<Icon
src={getItemIcon(containerType)}
screenReaderText={containerType}
title={containerType}
/>
</div>
<div className="text text-truncate">
{text}
</div>
{publishMessage && (
<Stack
direction="horizontal"
gap={2}
className="publish-status"
>
<Icon src={willPublish ? Check : Description} />
<FormattedMessage {...(willPublish ? messages.willPublishChipText : publishMessage)} />
</Stack>
)}
</Stack>
</Container>
{showArrow && (
<div
className={classNames('hierarchy-arrow', { selected })}
>
<Icon
src={ArrowDownward}
screenReaderText={' '}
/>
</div>
)}
</Stack>
);

const ContainerHierarchy = ({
showPublishStatus = false,
}: {
showPublishStatus?: boolean,
}) => {
const intl = useIntl();
const { sidebarItemInfo } = useSidebarContext();
const containerId = sidebarItemInfo?.id;

// istanbul ignore if: this should never happen
if (!containerId) {
throw new Error('containerId is required');
}

const {
data,
isLoading,
isError,
} = useContainerHierarchy(containerId);

if (isLoading) {
return <Loading />;
}

// istanbul ignore if: this should never happen
if (isError) {
return null;
}

const {
sections,
subsections,
units,
components,
} = data;

// Returns a message describing the publish status of the given hierarchy row.
const publishMessage = (contents: ContainerHierarchyMember[]) => {
// If we're not showing publish status, then we don't need a publish message
if (!showPublishStatus) {
return undefined;
}

// If any item has unpublished changes, mark this row as Draft.
if (contents.some((item) => item.hasUnpublishedChanges)) {
return messages.draftChipText;
}

// Otherwise, it's Published
return messages.publishedChipText;
};

// Returns True if any of the items in the list match the currently selected container.
const selected = (contents: ContainerHierarchyMember[]): boolean => (
contents.some((item) => item.id === containerId)
);

// Use the "selected" status to determine the selected row.
// If showPublishStatus, that row and its children will be marked "willPublish".
const selectedSections = selected(sections);
const selectedSubsections = selected(subsections);
const selectedUnits = selected(units);
const selectedComponents = selected(components);

const showSections = sections && sections.length > 0;
const showSubsections = subsections && subsections.length > 0;
const showUnits = units && units.length > 0;
const showComponents = components && components.length > 0;

return (
<Stack className="content-hierarchy">
{showSections && (
<ContainerHierarchyRow
containerType={ContainerType.Section}
text={intl.formatMessage(
messages.hierarchySections,
{
displayName: sections[0].displayName,
count: sections.length,
},
)}
showArrow={showSubsections}
selected={selectedSections}
willPublish={selectedSections}
publishMessage={publishMessage(sections)}
/>
)}
{showSubsections && (
<ContainerHierarchyRow
containerType={ContainerType.Subsection}
text={intl.formatMessage(
messages.hierarchySubsections,
{
displayName: subsections[0].displayName,
count: subsections.length,
},
)}
showArrow={showUnits}
selected={selectedSubsections}
willPublish={selectedSubsections || selectedSections}
publishMessage={publishMessage(subsections)}
/>
)}
{showUnits && (
<ContainerHierarchyRow
containerType={ContainerType.Unit}
text={intl.formatMessage(
messages.hierarchyUnits,
{
displayName: units[0].displayName,
count: units.length,
},
)}
showArrow={showComponents}
selected={selectedUnits}
willPublish={selectedUnits || selectedSubsections || selectedSections}
publishMessage={publishMessage(units)}
/>
)}
{showComponents && (
<ContainerHierarchyRow
containerType={ContainerType.Components}
text={intl.formatMessage(
messages.hierarchyComponents,
{
displayName: components[0].displayName,
count: components.length,
},
)}
showArrow={false}
selected={selectedComponents}
willPublish={selectedComponents || selectedUnits || selectedSubsections || selectedSections}
publishMessage={publishMessage(components)}
/>
)}
</Stack>
);
};

export default ContainerHierarchy;
Loading