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';
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
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(
<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();
});
});
});
131 changes: 131 additions & 0 deletions src/library-authoring/containers/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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';

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

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 mb-4.5" disabled={disableAll}>
<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" checked>
<FormattedMessage {...messages.settingsSectionEnableDiscussionLabel} />
</Form.Checkbox>
<p className="text-muted small mb-4">
<FormattedMessage {...messages.settingsSectionUnpublishedUnitsLabel} />
</p>
</>
)}
</div>
</>
);
};
Loading