Skip to content
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
86 changes: 50 additions & 36 deletions src/library-authoring/containers/ContainerInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
Button,
Stack,
Tab,
Tabs,
Dropdown,
Icon,
IconButton,
useToggle,
} from '@openedx/paragon';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Tab, Nav } from 'react-bootstrap';
Comment on lines +10 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the other PR:

We should not be directly importing from react-bootstrap. That's what the eslint warning import/no-extraneous-dependencies is trying to tell you.

Future versions of Paragon may be refactored to not use bootstrap at all, so we should only use Paragon APIs.

import React, { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { MoreVert } from '@openedx/paragon/icons';
Expand All @@ -24,6 +24,7 @@ import {
} from '../common/context/SidebarContext';
import ContainerOrganize from './ContainerOrganize';
import ContainerUsage from './ContainerUsage';
import { SettingsPanel } from './SettingsPanel';
import { useLibraryRoutes } from '../routes';
import { LibraryUnitBlocks } from '../units/LibraryUnitBlocks';
import { LibraryContainerChildren } from '../section-subsections/LibraryContainerChildren';
Expand All @@ -39,7 +40,6 @@ type ContainerPreviewProps = {

const ContainerMenu = ({ containerId }: ContainerPreviewProps) => {
const intl = useIntl();

const [isConfirmingDelete, confirmDelete, cancelDelete] = useToggle(false);

return (
Expand Down Expand Up @@ -159,23 +159,21 @@ const ContainerInfo = () => {
sidebarTab && isContainerInfoTab(sidebarTab)
) ? sidebarTab : defaultContainerTab;

/* istanbul ignore next */
const handleTabChange = (newTab: ContainerInfoTab) => {
resetSidebarAction();
setSidebarTab(newTab);
};

const renderTab = useCallback((infoTab: ContainerInfoTab, title: string, component?: React.ReactNode) => {
const renderTab = useCallback((infoTab: ContainerInfoTab, title: string) => {
if (hiddenTabs.includes(infoTab)) {
// For some reason, returning anything other than empty list breaks the tab style
return [];
return null;
}
return (
<Tab eventKey={infoTab} title={title}>
{component}
</Tab>
<Nav.Item key={infoTab}>
<Nav.Link eventKey={infoTab}>{title}</Nav.Link>
</Nav.Item>
);
}, [hiddenTabs, defaultContainerTab, containerId]);
}, [hiddenTabs]);

if (!container || !containerId || !containerType) {
return null;
Expand All @@ -188,34 +186,50 @@ const ContainerInfo = () => {
containerType={containerType}
hasUnpublishedChanges={container.hasUnpublishedChanges}
/>
<Tabs
variant="tabs"
className="my-3 d-flex justify-content-around"

<Tab.Container
defaultActiveKey={defaultContainerTab}
activeKey={tab}
onSelect={handleTabChange}
onSelect={(k) => handleTabChange(k as ContainerInfoTab)}
mountOnEnter
unmountOnExit
>
{renderTab(
CONTAINER_INFO_TABS.Preview,
intl.formatMessage(messages.previewTabTitle),
<ContainerPreview containerId={containerId} />,
)}
{renderTab(
CONTAINER_INFO_TABS.Manage,
intl.formatMessage(messages.manageTabTitle),
<ContainerOrganize />,
)}
{renderTab(
CONTAINER_INFO_TABS.Usage,
intl.formatMessage(messages.usageTabTitle),
<ContainerUsage />,
)}
{renderTab(
CONTAINER_INFO_TABS.Settings,
intl.formatMessage(messages.settingsTabTitle),
// TODO: container settings component
)}
</Tabs>
<Nav variant="tabs" className="my-3 d-flex justify-content-around">
{renderTab(
CONTAINER_INFO_TABS.Preview,
intl.formatMessage(messages.previewTabTitle),
)}
{renderTab(
CONTAINER_INFO_TABS.Manage,
intl.formatMessage(messages.manageTabTitle),
)}
{renderTab(
CONTAINER_INFO_TABS.Usage,
intl.formatMessage(messages.usageTabTitle),
)}
{/* 👇 Always show Settings */}
<Nav.Item>
<Nav.Link eventKey={CONTAINER_INFO_TABS.Settings}>
{intl.formatMessage(messages.settingsTabTitle)}
</Nav.Link>
</Nav.Item>
</Nav>

<Tab.Content className="mt-3">
<Tab.Pane eventKey={CONTAINER_INFO_TABS.Preview}>
<ContainerPreview containerId={containerId} />
</Tab.Pane>
<Tab.Pane eventKey={CONTAINER_INFO_TABS.Manage}>
<ContainerOrganize />
</Tab.Pane>
<Tab.Pane eventKey={CONTAINER_INFO_TABS.Usage}>
<ContainerUsage />
</Tab.Pane>
<Tab.Pane eventKey={CONTAINER_INFO_TABS.Settings}>
<SettingsPanel containerType={containerType} />
</Tab.Pane>
</Tab.Content>
</Tab.Container>
</Stack>
);
};
Expand Down
3 changes: 3 additions & 0 deletions src/library-authoring/containers/SettingsPanel.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.text-muted-override{
color: #DEDBDB !important;
}
122 changes: 122 additions & 0 deletions src/library-authoring/containers/SettingsPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { ContainerType } from '@src/generic/key-utils';
import { SettingsPanel } from './SettingsPanel';
import messages from './messages';

const renderWithIntl = (ui: React.ReactNode) => render(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually just use render from @src/testUtils which includes IntlProvider and more, but this is fine too.

<IntlProvider locale="en" messages={{}}>
{ui}
</IntlProvider>,
);

describe('SettingsPanel', () => {
describe('Section container', () => {
test('renders section default info text', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Section} />);
expect(
screen.getByText(messages.settingsSectionDefaultText.defaultMessage),
).toBeInTheDocument();
});

test('does not render grading or results visibility', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Section} />);
expect(
screen.queryByText(messages.settingsSectionGradingLabel.defaultMessage),
).not.toBeInTheDocument();
expect(
screen.queryByText(
messages.settingsSectionAssessmentResultsVisibilityLabel.defaultMessage,
),
).not.toBeInTheDocument();
});

test('renders visibility controls', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Section} />);
expect(
screen.getByText(messages.settingsSectionVisibilityLabel.defaultMessage),
).toBeInTheDocument();
expect(
screen.getByRole('button', {
name: messages.settingsSectionDefaultVisibilityButton.defaultMessage,
}),
).toBeDisabled();
});
});

describe('Subsection container', () => {
test('renders subsection default info text', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Subsection} />);
expect(
screen.getByText(messages.settingsSubSectionDefaultText.defaultMessage),
).toBeInTheDocument();
});

test('renders grading buttons (disabled)', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Subsection} />);
expect(
screen.getByRole('button', {
name: messages.settingsSectionUpgradeButton.defaultMessage,
}),
).toBeDisabled();
expect(
screen.getByRole('button', {
name: messages.settingsSectionGradeButton.defaultMessage,
}),
).toBeDisabled();
});

test('renders visibility + hide content checkbox', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Subsection} />);
expect(
screen.getByLabelText(
messages.settingsSectionHideContentAfterDueDateLabel.defaultMessage,
),
).toBeDisabled();
});

test('renders results visibility controls', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Subsection} />);
expect(
screen.getByRole('button', {
name: messages.settingsSectionShowButton.defaultMessage,
}),
).toBeDisabled();
expect(
screen.getByRole('button', {
name: messages.settingsSectionHideButton.defaultMessage,
}),
).toBeDisabled();
expect(
screen.getByLabelText(
messages.settingsSectionOnlyShowResultsAfterDueDateLabel.defaultMessage,
),
).toBeDisabled();
});
});

describe('Unit container', () => {
test('renders unit default info text', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Unit} />);
expect(
screen.getByText(messages.settingsUnitDefaultText.defaultMessage),
).toBeInTheDocument();
});

test('renders discussion settings', () => {
renderWithIntl(<SettingsPanel containerType={ContainerType.Unit} />);
expect(
screen.getByText(messages.settingsSectionDiscussionLabel.defaultMessage),
).toBeInTheDocument();
expect(
screen.getByLabelText(
messages.settingsSectionEnableDiscussionLabel.defaultMessage,
),
).toBeChecked();
expect(
screen.getByText(messages.settingsSectionUnpublishedUnitsLabel.defaultMessage),
).toBeInTheDocument();
});
});
});
133 changes: 133 additions & 0 deletions src/library-authoring/containers/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { useState } from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button, ButtonGroup, Form } from '@openedx/paragon';
import { ContainerType } from '@src/generic/key-utils';
import messages from './messages';

import './SettingsPanel.scss';

interface SettingsPanelProps {
containerType: string;
}

export const SettingsPanel: React.FC<SettingsPanelProps> = ({ containerType }) => {
const [grading] = useState('ungraded');
const [visibility] = useState('default');
const [resultsVisibility] = useState('show');

const disableAll = true; // 👈 set to false to re-enable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this disableAll ?


return (
<>
<div className="pb-2 pl-4 pr-4 space-y-4">
<p className="text-muted small mb-4">
{ containerType === ContainerType.Section && (
<FormattedMessage {...messages.settingsSectionDefaultText} />
)}
{ containerType === ContainerType.Subsection && (
<FormattedMessage {...messages.settingsSubSectionDefaultText} />
)}
{ containerType === ContainerType.Unit && (
<FormattedMessage {...messages.settingsUnitDefaultText} />
)}
</p>
</div>
<div className="pb-4 pl-4 pr-4 space-y-4">
{containerType === ContainerType.Subsection && (
<>
<h6 className="text-muted small font-weight-bold mb-3">
<FormattedMessage {...messages.settingsSectionGradingLabel} />
</h6>
<ButtonGroup className="d-flex w-100 mb-4.5">
<Button
className="flex-fill"
variant={grading === 'ungraded' ? 'dark' : 'outline-secondary'}
size="sm"
disabled={disableAll}
>
<FormattedMessage {...messages.settingsSectionUpgradeButton} />
</Button>
<Button
className="flex-fill"
variant={grading === 'graded' ? 'dark' : 'outline-secondary'}
size="sm"
disabled={disableAll}
>
<FormattedMessage {...messages.settingsSectionGradeButton} />
</Button>
</ButtonGroup>
</>
)}

<h6 className="text-muted small font-weight-bold mt-3 mb-3">
<FormattedMessage {...messages.settingsSectionVisibilityLabel} />
</h6>
<ButtonGroup className="d-flex w-100">
<Button
className="flex-fill"
variant={visibility === 'default' ? 'dark' : 'outline-secondary'}
size="sm"
disabled={disableAll}
>
<FormattedMessage {...messages.settingsSectionDefaultVisibilityButton} />
</Button>
<Button
className="flex-fill"
variant={visibility === 'staff' ? 'dark' : 'outline-secondary'}
size="sm"
disabled={disableAll}
>
<FormattedMessage {...messages.settingsSectionStaffOnlyButton} />
</Button>
</ButtonGroup>
{containerType === ContainerType.Subsection && (
<Form.Checkbox className="mt-3 text-muted mb-4.5" disabled>
<FormattedMessage {...messages.settingsSectionHideContentAfterDueDateLabel} />
</Form.Checkbox>
)}

{containerType === ContainerType.Subsection && (
<>
<h6 className="text-muted small font-weight-bold mt-1 mb-3">
<FormattedMessage {...messages.settingsSectionAssessmentResultsVisibilityLabel} />
</h6>
<ButtonGroup className="d-flex w-100">
<Button
className="flex-fill"
variant={resultsVisibility === 'show' ? 'dark' : 'outline-secondary'}
size="sm"
disabled={disableAll}
>
<FormattedMessage {...messages.settingsSectionShowButton} />
</Button>
<Button
className="flex-fill"
variant={resultsVisibility === 'hide' ? 'dark' : 'outline-secondary'}
size="sm"
disabled={disableAll}
>
<FormattedMessage {...messages.settingsSectionHideButton} />
</Button>
</ButtonGroup>
<Form.Checkbox className="mt-3 text-muted mb-4.5" disabled>
<FormattedMessage {...messages.settingsSectionOnlyShowResultsAfterDueDateLabel} />
</Form.Checkbox>
</>
)}
{containerType === ContainerType.Unit && (
<>
<h6 className="text-muted small font-weight-bold mt-3 mb-3">
<FormattedMessage {...messages.settingsSectionDiscussionLabel} />
</h6>
<Form.Checkbox className="mt-3 text-muted" disabled checked>
<FormattedMessage {...messages.settingsSectionEnableDiscussionLabel} />
</Form.Checkbox>
<p className="text-muted small mb-4 text-muted-override">
<FormattedMessage {...messages.settingsSectionUnpublishedUnitsLabel} />
</p>
</>
)}
</div>
</>
);
};
Loading