Skip to content

Commit 2f6e510

Browse files
authored
Display Container Publish status and confirm before publish (#2186)
Updates the Container sidebar to display: * A confirmation step before publishing the container. * Text + a full hierarchy to better demonstrate what will be published when the container is published.
1 parent 87af7e8 commit 2f6e510

22 files changed

+1081
-86
lines changed

src/generic/Loading.jsx renamed to src/generic/Loading.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import React from 'react';
2-
import PropTypes from 'prop-types';
31
import { Spinner } from '@openedx/paragon';
42
import { FormattedMessage } from '@edx/frontend-platform/i18n';
53

6-
export const LoadingSpinner = ({ size }) => (
4+
interface LoadingSpinnerProps {
5+
size?: string;
6+
}
7+
8+
export const LoadingSpinner = ({ size }: LoadingSpinnerProps) => (
79
<Spinner
810
animation="border"
911
role="status"
@@ -19,14 +21,6 @@ export const LoadingSpinner = ({ size }) => (
1921
/>
2022
);
2123

22-
LoadingSpinner.defaultProps = {
23-
size: undefined,
24-
};
25-
26-
LoadingSpinner.propTypes = {
27-
size: PropTypes.string,
28-
};
29-
3024
const Loading = () => (
3125
<div className="d-flex justify-content-center align-items-center flex-column vh-100">
3226
<LoadingSpinner />

src/generic/block-type-utils/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Folder,
1717
ViewCarousel,
1818
ViewDay,
19+
Widgets,
1920
WidthWide,
2021
} from '@openedx/paragon/icons';
2122
import NewsstandIcon from '../NewsstandIcon';
@@ -43,6 +44,7 @@ export const UNIT_TYPE_ICONS_MAP: Record<string, React.ComponentType> = {
4344
chapter: ViewCarousel,
4445
problem: EditIcon,
4546
lock: LockIcon,
47+
multiple: Widgets,
4648
};
4749

4850
export const COMPONENT_TYPE_ICON_MAP: Record<string, React.ComponentType> = {
@@ -65,6 +67,7 @@ export const STRUCTURAL_TYPE_ICONS: Record<string, React.ComponentType> = {
6567
subsection: UNIT_TYPE_ICONS_MAP.sequential,
6668
chapter: UNIT_TYPE_ICONS_MAP.chapter,
6769
section: UNIT_TYPE_ICONS_MAP.chapter,
70+
components: UNIT_TYPE_ICONS_MAP.multiple,
6871
collection: Folder,
6972
libraryContent: Folder,
7073
paste: ContentPasteIcon,

src/generic/key-utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,9 @@ export enum ContainerType {
7070
Chapter = 'chapter',
7171
Sequential = 'sequential',
7272
Vertical = 'vertical',
73+
/**
74+
* Components are not strictly a container type, but we add this here for simplicity when rendering the container
75+
* hierarchy.
76+
*/
77+
Components = 'components',
7378
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.content-hierarchy {
2+
margin-bottom: var(--pgn-spacing-paragraph-margin-bottom);
3+
4+
.hierarchy-row {
5+
border: 1px solid var(--pgn-color-light-500);
6+
border-radius: 4px;
7+
background-color: var(--pgn-color-white);
8+
padding: 0;
9+
margin: 0;
10+
11+
&.selected {
12+
border: 3px solid var(--pgn-color-primary-500);
13+
border-radius: 4px;
14+
}
15+
16+
.icon {
17+
background-color: var(--pgn-color-light-300);
18+
border-top: 2px solid var(--pgn-color-light-300);
19+
border-bottom: 2px solid var(--pgn-color-light-300);
20+
border-right: 1px solid var(--pgn-color-light-500);
21+
border-radius: 1px 0 0 1px;
22+
padding: 8px 12px;
23+
}
24+
25+
&.selected .icon {
26+
background-color: var(--pgn-color-primary-500);
27+
border-color: var(--pgn-color-primary-500);
28+
color: var(--pgn-color-white);
29+
}
30+
31+
.text {
32+
padding: 8px 12px;
33+
flex-grow: 2;
34+
}
35+
36+
.publish-status {
37+
background-color: var(--pgn-color-info-200);
38+
white-space: nowrap;
39+
padding: 8px 12px;
40+
}
41+
}
42+
43+
.hierarchy-arrow {
44+
color: var(--pgn-color-light-500);
45+
padding: 0 0 0 14px;
46+
position: relative;
47+
top: -4px;
48+
height: 20px;
49+
50+
&.selected {
51+
color: var(--pgn-color-primary-500);
52+
}
53+
}
54+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import type { MessageDescriptor } from 'react-intl';
2+
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
3+
import { Container, Icon, Stack } from '@openedx/paragon';
4+
import { ArrowDownward, Check, Description } from '@openedx/paragon/icons';
5+
import classNames from 'classnames';
6+
import { getItemIcon } from '@src/generic/block-type-utils';
7+
import Loading from '@src/generic/Loading';
8+
import { ContainerType } from '@src/generic/key-utils';
9+
import type { ContainerHierarchyMember } from '../data/api';
10+
import { useContainerHierarchy } from '../data/apiHooks';
11+
import { useSidebarContext } from '../common/context/SidebarContext';
12+
import messages from './messages';
13+
14+
const ContainerHierarchyRow = ({
15+
containerType,
16+
text,
17+
selected,
18+
showArrow,
19+
willPublish = false,
20+
publishMessage = undefined,
21+
}: {
22+
containerType: ContainerType,
23+
text: string,
24+
selected: boolean,
25+
showArrow: boolean,
26+
willPublish?: boolean,
27+
publishMessage?: MessageDescriptor,
28+
}) => (
29+
<Stack>
30+
<Container
31+
className={classNames('hierarchy-row', { selected })}
32+
>
33+
<Stack
34+
direction="horizontal"
35+
gap={2}
36+
>
37+
<div className="icon">
38+
<Icon
39+
src={getItemIcon(containerType)}
40+
screenReaderText={containerType}
41+
title={containerType}
42+
/>
43+
</div>
44+
<div className="text text-truncate">
45+
{text}
46+
</div>
47+
{publishMessage && (
48+
<Stack
49+
direction="horizontal"
50+
gap={2}
51+
className="publish-status"
52+
>
53+
<Icon src={willPublish ? Check : Description} />
54+
<FormattedMessage {...(willPublish ? messages.willPublishChipText : publishMessage)} />
55+
</Stack>
56+
)}
57+
</Stack>
58+
</Container>
59+
{showArrow && (
60+
<div
61+
className={classNames('hierarchy-arrow', { selected })}
62+
>
63+
<Icon
64+
src={ArrowDownward}
65+
screenReaderText={' '}
66+
/>
67+
</div>
68+
)}
69+
</Stack>
70+
);
71+
72+
const ContainerHierarchy = ({
73+
showPublishStatus = false,
74+
}: {
75+
showPublishStatus?: boolean,
76+
}) => {
77+
const intl = useIntl();
78+
const { sidebarItemInfo } = useSidebarContext();
79+
const containerId = sidebarItemInfo?.id;
80+
81+
// istanbul ignore if: this should never happen
82+
if (!containerId) {
83+
throw new Error('containerId is required');
84+
}
85+
86+
const {
87+
data,
88+
isLoading,
89+
isError,
90+
} = useContainerHierarchy(containerId);
91+
92+
if (isLoading) {
93+
return <Loading />;
94+
}
95+
96+
// istanbul ignore if: this should never happen
97+
if (isError) {
98+
return null;
99+
}
100+
101+
const {
102+
sections,
103+
subsections,
104+
units,
105+
components,
106+
} = data;
107+
108+
// Returns a message describing the publish status of the given hierarchy row.
109+
const publishMessage = (contents: ContainerHierarchyMember[]) => {
110+
// If we're not showing publish status, then we don't need a publish message
111+
if (!showPublishStatus) {
112+
return undefined;
113+
}
114+
115+
// If any item has unpublished changes, mark this row as Draft.
116+
if (contents.some((item) => item.hasUnpublishedChanges)) {
117+
return messages.draftChipText;
118+
}
119+
120+
// Otherwise, it's Published
121+
return messages.publishedChipText;
122+
};
123+
124+
// Returns True if any of the items in the list match the currently selected container.
125+
const selected = (contents: ContainerHierarchyMember[]): boolean => (
126+
contents.some((item) => item.id === containerId)
127+
);
128+
129+
// Use the "selected" status to determine the selected row.
130+
// If showPublishStatus, that row and its children will be marked "willPublish".
131+
const selectedSections = selected(sections);
132+
const selectedSubsections = selected(subsections);
133+
const selectedUnits = selected(units);
134+
const selectedComponents = selected(components);
135+
136+
const showSections = sections && sections.length > 0;
137+
const showSubsections = subsections && subsections.length > 0;
138+
const showUnits = units && units.length > 0;
139+
const showComponents = components && components.length > 0;
140+
141+
return (
142+
<Stack className="content-hierarchy">
143+
{showSections && (
144+
<ContainerHierarchyRow
145+
containerType={ContainerType.Section}
146+
text={intl.formatMessage(
147+
messages.hierarchySections,
148+
{
149+
displayName: sections[0].displayName,
150+
count: sections.length,
151+
},
152+
)}
153+
showArrow={showSubsections}
154+
selected={selectedSections}
155+
willPublish={selectedSections}
156+
publishMessage={publishMessage(sections)}
157+
/>
158+
)}
159+
{showSubsections && (
160+
<ContainerHierarchyRow
161+
containerType={ContainerType.Subsection}
162+
text={intl.formatMessage(
163+
messages.hierarchySubsections,
164+
{
165+
displayName: subsections[0].displayName,
166+
count: subsections.length,
167+
},
168+
)}
169+
showArrow={showUnits}
170+
selected={selectedSubsections}
171+
willPublish={selectedSubsections || selectedSections}
172+
publishMessage={publishMessage(subsections)}
173+
/>
174+
)}
175+
{showUnits && (
176+
<ContainerHierarchyRow
177+
containerType={ContainerType.Unit}
178+
text={intl.formatMessage(
179+
messages.hierarchyUnits,
180+
{
181+
displayName: units[0].displayName,
182+
count: units.length,
183+
},
184+
)}
185+
showArrow={showComponents}
186+
selected={selectedUnits}
187+
willPublish={selectedUnits || selectedSubsections || selectedSections}
188+
publishMessage={publishMessage(units)}
189+
/>
190+
)}
191+
{showComponents && (
192+
<ContainerHierarchyRow
193+
containerType={ContainerType.Components}
194+
text={intl.formatMessage(
195+
messages.hierarchyComponents,
196+
{
197+
displayName: components[0].displayName,
198+
count: components.length,
199+
},
200+
)}
201+
showArrow={false}
202+
selected={selectedComponents}
203+
willPublish={selectedComponents || selectedUnits || selectedSubsections || selectedSections}
204+
publishMessage={publishMessage(components)}
205+
/>
206+
)}
207+
</Stack>
208+
);
209+
};
210+
211+
export default ContainerHierarchy;

0 commit comments

Comments
 (0)