Compare commits
21 Commits
master
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d43579321b | ||
|
|
4aed4dee15 | ||
|
|
0189e919a6 | ||
|
|
65e431cebe | ||
|
|
af4e25b39f | ||
|
|
0589912714 | ||
|
|
4ff3684772 | ||
|
|
0ae7aa0265 | ||
|
|
dc7bb9fe04 | ||
|
|
8abcbe0385 | ||
|
|
455656265e | ||
|
|
8518933970 | ||
|
|
d80a68132a | ||
|
|
b66238c7c0 | ||
|
|
e4c5238f70 | ||
|
|
1bc759a1e7 | ||
|
|
5cc04f8a80 | ||
|
|
de4189b4a5 | ||
|
|
785b91d3c7 | ||
|
|
a63409eaa6 | ||
|
|
3c8e5b2501 |
1
.env
1
.env
@@ -34,6 +34,7 @@ ENABLE_UNIT_PAGE=false
|
|||||||
ENABLE_ASSETS_PAGE=false
|
ENABLE_ASSETS_PAGE=false
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
|
||||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||||
|
ENABLE_CERTIFICATE_PAGE=true
|
||||||
BBB_LEARN_MORE_URL=''
|
BBB_LEARN_MORE_URL=''
|
||||||
HOTJAR_APP_ID=''
|
HOTJAR_APP_ID=''
|
||||||
HOTJAR_VERSION=6
|
HOTJAR_VERSION=6
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ ENABLE_TEAM_TYPE_SETTING=false
|
|||||||
ENABLE_UNIT_PAGE=false
|
ENABLE_UNIT_PAGE=false
|
||||||
ENABLE_ASSETS_PAGE=false
|
ENABLE_ASSETS_PAGE=false
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||||
|
ENABLE_CERTIFICATE_PAGE=true
|
||||||
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
|
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
|
||||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||||
BBB_LEARN_MORE_URL=''
|
BBB_LEARN_MORE_URL=''
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ ENABLE_TEAM_TYPE_SETTING=false
|
|||||||
ENABLE_UNIT_PAGE=true
|
ENABLE_UNIT_PAGE=true
|
||||||
ENABLE_ASSETS_PAGE=false
|
ENABLE_ASSETS_PAGE=false
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||||
|
ENABLE_CERTIFICATE_PAGE=true
|
||||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||||
BBB_LEARN_MORE_URL=''
|
BBB_LEARN_MORE_URL=''
|
||||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -18,7 +18,7 @@
|
|||||||
"@edx/frontend-component-footer": "^13.0.2",
|
"@edx/frontend-component-footer": "^13.0.2",
|
||||||
"@edx/frontend-component-header": "^5.1.0",
|
"@edx/frontend-component-header": "^5.1.0",
|
||||||
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||||
"@edx/frontend-lib-content-components": "^2.1.7",
|
"@edx/frontend-lib-content-components": "2.5.3",
|
||||||
"@edx/frontend-platform": "7.0.1",
|
"@edx/frontend-platform": "7.0.1",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@edx/openedx-atlas": "^0.6.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||||
@@ -2580,9 +2580,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-lib-content-components": {
|
"node_modules/@edx/frontend-lib-content-components": {
|
||||||
"version": "2.1.7",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-content-components/-/frontend-lib-content-components-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-content-components/-/frontend-lib-content-components-2.5.3.tgz",
|
||||||
"integrity": "sha512-RjE263H/GabHmEe5EFaku7LSngkJitVbnWSxvRhsmO2o5LWwEctUcpkQaK7YCN6fpAlqXmcXVMrtM/lzP4j2Bw==",
|
"integrity": "sha512-B9/UlnDBhMUjvosvsZ0a8Kga/DdWA6PpNOHi2r0w+xc2U6jWKzpO/ZFdNQMWv6ZVIRbSAIpKy2swSDiCWXskxw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-html": "^6.0.0",
|
"@codemirror/lang-html": "^6.0.0",
|
||||||
"@codemirror/lang-xml": "^6.0.0",
|
"@codemirror/lang-xml": "^6.0.0",
|
||||||
@@ -2621,7 +2621,7 @@
|
|||||||
"xmlchecker": "^0.1.0"
|
"xmlchecker": "^0.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@edx/frontend-platform": "^7.0.1",
|
"@edx/frontend-platform": "^7.0.1 || ^8.0.0",
|
||||||
"@openedx/paragon": "^21.5.7 || ^22.0.0",
|
"@openedx/paragon": "^21.5.7 || ^22.0.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^16.14.0 || ^17.0.0",
|
"react": "^16.14.0 || ^17.0.0",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"@edx/frontend-component-footer": "^13.0.2",
|
"@edx/frontend-component-footer": "^13.0.2",
|
||||||
"@edx/frontend-component-header": "^5.1.0",
|
"@edx/frontend-component-header": "^5.1.0",
|
||||||
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||||
"@edx/frontend-lib-content-components": "^2.1.7",
|
"@edx/frontend-lib-content-components": "2.5.3",
|
||||||
"@edx/frontend-platform": "7.0.1",
|
"@edx/frontend-platform": "7.0.1",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@edx/openedx-atlas": "^0.6.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const CourseAuthoringRoutes = () => {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="certificates"
|
path="certificates"
|
||||||
element={<PageWrap><Certificates courseId={courseId} /></PageWrap>}
|
element={getConfig().ENABLE_CERTIFICATE_PAGE === 'true' ? <PageWrap><Certificates courseId={courseId} /></PageWrap> : null}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="textbooks"
|
path="textbooks"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const SettingCard = ({
|
|||||||
iconAs={Icon}
|
iconAs={Icon}
|
||||||
alt={intl.formatMessage(messages.helpButtonText)}
|
alt={intl.formatMessage(messages.helpButtonText)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className=" ml-1 mr-2"
|
className="flex-shrink-0 ml-1 mr-2"
|
||||||
/>
|
/>
|
||||||
<ModalPopup
|
<ModalPopup
|
||||||
hasArrow
|
hasArrow
|
||||||
|
|||||||
@@ -79,3 +79,7 @@
|
|||||||
color: $black;
|
color: $black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-datepicker-popper {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,14 +8,72 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Toast,
|
Toast,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ContentTagsCollapsible from './ContentTagsCollapsible';
|
import ContentTagsCollapsible from './ContentTagsCollapsible';
|
||||||
import Loading from '../generic/Loading';
|
import Loading from '../generic/Loading';
|
||||||
import useContentTagsDrawerContext from './ContentTagsDrawerHelper';
|
import useContentTagsDrawerContext from './ContentTagsDrawerHelper';
|
||||||
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
|
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
|
||||||
|
|
||||||
|
const TaxonomyList = ({ contentId }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isTaxonomyListLoaded,
|
||||||
|
isContentTaxonomyTagsLoaded,
|
||||||
|
tagsByTaxonomy,
|
||||||
|
stagedContentTags,
|
||||||
|
collapsibleStates,
|
||||||
|
} = React.useContext(ContentTagsDrawerContext);
|
||||||
|
|
||||||
|
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
|
||||||
|
if (tagsByTaxonomy.length !== 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ tagsByTaxonomy.map((data) => (
|
||||||
|
<div key={`taxonomy-tags-collapsible-${data.id}`}>
|
||||||
|
<ContentTagsCollapsible
|
||||||
|
contentId={contentId}
|
||||||
|
taxonomyAndTagsData={data}
|
||||||
|
stagedContentTags={stagedContentTags[data.id] || []}
|
||||||
|
collapsibleState={collapsibleStates[data.id] || false}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.emptyDrawerContent}
|
||||||
|
values={{
|
||||||
|
link: (
|
||||||
|
<Button
|
||||||
|
tabIndex="0"
|
||||||
|
size="inline"
|
||||||
|
variant="link"
|
||||||
|
className="text-info-500 p-0 enable-taxonomies-button"
|
||||||
|
onClick={() => navigate('/taxonomies')}
|
||||||
|
>
|
||||||
|
{ intl.formatMessage(messages.emptyDrawerContentLink) }
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Loading />;
|
||||||
|
};
|
||||||
|
|
||||||
|
TaxonomyList.propTypes = {
|
||||||
|
contentId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drawer with the functionality to show and manage tags in a certain content.
|
* Drawer with the functionality to show and manage tags in a certain content.
|
||||||
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
|
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
|
||||||
@@ -42,7 +100,6 @@ const ContentTagsDrawer = ({ id, onClose }) => {
|
|||||||
contentName,
|
contentName,
|
||||||
isTaxonomyListLoaded,
|
isTaxonomyListLoaded,
|
||||||
isContentTaxonomyTagsLoaded,
|
isContentTaxonomyTagsLoaded,
|
||||||
tagsByTaxonomy,
|
|
||||||
stagedContentTags,
|
stagedContentTags,
|
||||||
collapsibleStates,
|
collapsibleStates,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
@@ -110,19 +167,7 @@ const ContentTagsDrawer = ({ id, onClose }) => {
|
|||||||
<p className="h4 text-gray-500 font-weight-bold">
|
<p className="h4 text-gray-500 font-weight-bold">
|
||||||
{intl.formatMessage(messages.headerSubtitle)}
|
{intl.formatMessage(messages.headerSubtitle)}
|
||||||
</p>
|
</p>
|
||||||
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded
|
<TaxonomyList contentId={contentId} />
|
||||||
? tagsByTaxonomy.map((data) => (
|
|
||||||
<div key={`taxonomy-tags-collapsible-${data.id}`}>
|
|
||||||
<ContentTagsCollapsible
|
|
||||||
contentId={contentId}
|
|
||||||
taxonomyAndTagsData={data}
|
|
||||||
stagedContentTags={stagedContentTags[data.id] || []}
|
|
||||||
collapsibleState={collapsibleStates[data.id] || false}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: <Loading />}
|
|
||||||
{otherTaxonomies.length !== 0 && (
|
{otherTaxonomies.length !== 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="h4 text-gray-500 font-weight-bold">
|
<p className="h4 text-gray-500 font-weight-bold">
|
||||||
|
|||||||
@@ -22,6 +22,11 @@
|
|||||||
.other-description {
|
.other-description {
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enable-taxonomies-button:not([disabled]):hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: $info-900 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply styles to sheet only if it has a child with a .tags-drawer class
|
// Apply styles to sheet only if it has a child with a .tags-drawer class
|
||||||
|
|||||||
@@ -20,17 +20,20 @@ import {
|
|||||||
import { getTaxonomyListData } from '../taxonomy/data/api';
|
import { getTaxonomyListData } from '../taxonomy/data/api';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { ContentTagsDrawerSheetContext } from './common/context';
|
import { ContentTagsDrawerSheetContext } from './common/context';
|
||||||
|
import { languageExportId } from './utils';
|
||||||
|
|
||||||
const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab';
|
const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab';
|
||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
const mockMutate = jest.fn();
|
const mockMutate = jest.fn();
|
||||||
const mockSetBlockingSheet = jest.fn();
|
const mockSetBlockingSheet = jest.fn();
|
||||||
|
const mockNavigate = jest.fn();
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
useParams: () => ({
|
useParams: () => ({
|
||||||
contentId,
|
contentId,
|
||||||
}),
|
}),
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// FIXME: replace these mocks with API mocks
|
// FIXME: replace these mocks with API mocks
|
||||||
@@ -256,6 +259,83 @@ describe('<ContentTagsDrawer />', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setupMockDataLanguageTaxonomyTestings = (hasTags) => {
|
||||||
|
useContentTaxonomyTagsData.mockReturnValue({
|
||||||
|
isSuccess: true,
|
||||||
|
data: {
|
||||||
|
taxonomies: [
|
||||||
|
{
|
||||||
|
name: 'Languages',
|
||||||
|
taxonomyId: 123,
|
||||||
|
exportId: languageExportId,
|
||||||
|
canTagObject: true,
|
||||||
|
tags: hasTags ? [
|
||||||
|
{
|
||||||
|
value: 'Tag 1',
|
||||||
|
lineage: ['Tag 1'],
|
||||||
|
canDeleteObjecttag: true,
|
||||||
|
},
|
||||||
|
] : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Taxonomy 1',
|
||||||
|
taxonomyId: 1234,
|
||||||
|
canTagObject: true,
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
value: 'Tag 1',
|
||||||
|
lineage: ['Tag 1'],
|
||||||
|
canDeleteObjecttag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Tag 2',
|
||||||
|
lineage: ['Tag 2'],
|
||||||
|
canDeleteObjecttag: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
getTaxonomyListData.mockResolvedValue({
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 123,
|
||||||
|
name: 'Languages',
|
||||||
|
description: 'This is a description 1',
|
||||||
|
exportId: languageExportId,
|
||||||
|
canTagObject: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1234,
|
||||||
|
name: 'Taxonomy 1',
|
||||||
|
description: 'This is a description 2',
|
||||||
|
canTagObject: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
useTaxonomyTagsData.mockReturnValue({
|
||||||
|
hasMorePages: false,
|
||||||
|
canAddTag: false,
|
||||||
|
tagPages: {
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
data: [{
|
||||||
|
value: 'Tag 1',
|
||||||
|
externalId: null,
|
||||||
|
childCount: 0,
|
||||||
|
depth: 0,
|
||||||
|
parentValue: null,
|
||||||
|
id: 12345,
|
||||||
|
subTagsUrl: null,
|
||||||
|
canChangeTag: false,
|
||||||
|
canDeleteTag: false,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setupLargeMockDataForStagedTagsTesting = () => {
|
const setupLargeMockDataForStagedTagsTesting = () => {
|
||||||
useContentTaxonomyTagsData.mockReturnValue({
|
useContentTaxonomyTagsData.mockReturnValue({
|
||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
@@ -1057,4 +1137,47 @@ describe('<ContentTagsDrawer />', () => {
|
|||||||
|
|
||||||
expect(screen.getByText(/tag 3/i)).toBeInTheDocument();
|
expect(screen.getByText(/tag 3/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show Language Taxonomy', async () => {
|
||||||
|
setupMockDataLanguageTaxonomyTestings(true);
|
||||||
|
render(<RootWrapper />);
|
||||||
|
expect(await screen.findByText('Languages')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide Language Taxonomy', async () => {
|
||||||
|
setupMockDataLanguageTaxonomyTestings(false);
|
||||||
|
render(<RootWrapper />);
|
||||||
|
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByText('Languages')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show empty drawer message', async () => {
|
||||||
|
useContentTaxonomyTagsData.mockReturnValue({
|
||||||
|
isSuccess: true,
|
||||||
|
data: {
|
||||||
|
taxonomies: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
getTaxonomyListData.mockResolvedValue({
|
||||||
|
results: [],
|
||||||
|
});
|
||||||
|
useTaxonomyTagsData.mockReturnValue({
|
||||||
|
hasMorePages: false,
|
||||||
|
canAddTag: false,
|
||||||
|
tagPages: {
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RootWrapper />);
|
||||||
|
expect(await screen.findByText(/to use tags, please or contact your administrator\./i)).toBeInTheDocument();
|
||||||
|
const enableButton = screen.getByRole('button', {
|
||||||
|
name: /enable a taxonomy/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(enableButton);
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/taxonomies');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { useContentData, useContentTaxonomyTagsData, useContentTaxonomyTagsUpdater } from './data/apiHooks';
|
import { useContentData, useContentTaxonomyTagsData, useContentTaxonomyTagsUpdater } from './data/apiHooks';
|
||||||
import { useTaxonomyList } from '../taxonomy/data/apiHooks';
|
import { useTaxonomyList } from '../taxonomy/data/apiHooks';
|
||||||
import { extractOrgFromContentId } from './utils';
|
import { extractOrgFromContentId, languageExportId } from './utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { ContentTagsDrawerSheetContext } from './common/context';
|
import { ContentTagsDrawerSheetContext } from './common/context';
|
||||||
|
|
||||||
@@ -142,8 +142,14 @@ const useContentTagsDrawerContext = (contentId) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete Language taxonomy if is empty
|
||||||
|
const filteredTaxonomies = taxonomiesList.filter(
|
||||||
|
(taxonomy) => taxonomy.exportId !== languageExportId
|
||||||
|
|| taxonomy.contentTags.length !== 0,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fechedTaxonomies: sortTaxonomies(taxonomiesList),
|
fechedTaxonomies: sortTaxonomies(filteredTaxonomies),
|
||||||
fechedOtherTaxonomies: otherTaxonomiesList,
|
fechedOtherTaxonomies: otherTaxonomiesList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,7 +323,9 @@ const ContentTagsDropDownSelector = ({
|
|||||||
|
|
||||||
{ tagPages.data.length === 0 && !tagPages.isLoading && (
|
{ tagPages.data.length === 0 && !tagPages.isLoading && (
|
||||||
<div className="d-flex justify-content-center muted-text">
|
<div className="d-flex justify-content-center muted-text">
|
||||||
<FormattedMessage {...messages.noTagsFoundMessage} values={{ searchTerm }} />
|
{ searchTerm
|
||||||
|
? <FormattedMessage {...messages.noTagsFoundMessage} values={{ searchTerm }} />
|
||||||
|
: <FormattedMessage {...messages.noTagsInTaxonomyMessage} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -282,4 +282,28 @@ describe('<ContentTagsDropDownSelector />', () => {
|
|||||||
expect(getByText(message)).toBeInTheDocument();
|
expect(getByText(message)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render "noTagsInTaxonomy" message if taxonomy is empty', async () => {
|
||||||
|
useTaxonomyTagsData.mockReturnValueOnce({
|
||||||
|
hasMorePages: false,
|
||||||
|
tagPages: {
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
isSuccess: true,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchTerm = '';
|
||||||
|
await act(async () => {
|
||||||
|
const { getByText } = await getComponent({ ...data, searchTerm });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm);
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = 'No tags in this taxonomy yet';
|
||||||
|
expect(getByText(message)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.no-tags-found',
|
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.no-tags-found',
|
||||||
defaultMessage: 'No tags found with the search term "{searchTerm}"',
|
defaultMessage: 'No tags found with the search term "{searchTerm}"',
|
||||||
},
|
},
|
||||||
|
noTagsInTaxonomyMessage: {
|
||||||
|
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.no-tags-in-taxonomy',
|
||||||
|
defaultMessage: 'No tags in this taxonomy yet',
|
||||||
|
description: 'Message when the user uses the tags dropdown selector of an empty taxonomy',
|
||||||
|
},
|
||||||
taxonomyTagChecked: {
|
taxonomyTagChecked: {
|
||||||
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.tag-checked',
|
id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.tag-checked',
|
||||||
defaultMessage: 'Checked',
|
defaultMessage: 'Checked',
|
||||||
@@ -124,6 +129,16 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'These tags are already applied, but you can\'t add new ones as you don\'t have access to their taxonomies.',
|
defaultMessage: 'These tags are already applied, but you can\'t add new ones as you don\'t have access to their taxonomies.',
|
||||||
description: 'Description of "Other tags" subsection in tags drawer',
|
description: 'Description of "Other tags" subsection in tags drawer',
|
||||||
},
|
},
|
||||||
|
emptyDrawerContent: {
|
||||||
|
id: 'course-authoring.content-tags-drawer.empty',
|
||||||
|
defaultMessage: 'To use tags, please {link} or contact your administrator.',
|
||||||
|
description: 'Message when there are no taxonomies.',
|
||||||
|
},
|
||||||
|
emptyDrawerContentLink: {
|
||||||
|
id: 'course-authoring.content-tags-drawer.empty-link',
|
||||||
|
defaultMessage: 'enable a taxonomy',
|
||||||
|
description: 'Message of the link used in empty drawer message.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const extractOrgFromContentId = (contentId) => contentId.split('+')[0].split(':')[1];
|
export const extractOrgFromContentId = (contentId) => contentId.split('+')[0].split(':')[1];
|
||||||
|
export const languageExportId = 'languages-v1';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Layout,
|
Layout,
|
||||||
Row,
|
Row,
|
||||||
TransitionReplace,
|
TransitionReplace,
|
||||||
|
Toast,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
SortableContext,
|
SortableContext,
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { LoadingSpinner } from '../generic/Loading';
|
import { LoadingSpinner } from '../generic/Loading';
|
||||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||||
@@ -52,9 +54,11 @@ import {
|
|||||||
} from '../generic/drag-helper/utils';
|
} from '../generic/drag-helper/utils';
|
||||||
import { useCourseOutline } from './hooks';
|
import { useCourseOutline } from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { getTagsExportFile } from './data/api';
|
||||||
|
|
||||||
const CourseOutline = ({ courseId }) => {
|
const CourseOutline = ({ courseId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
courseName,
|
courseName,
|
||||||
@@ -117,6 +121,23 @@ const CourseOutline = ({ courseId }) => {
|
|||||||
errors,
|
errors,
|
||||||
} = useCourseOutline({ courseId });
|
} = useCourseOutline({ courseId });
|
||||||
|
|
||||||
|
// Use `setToastMessage` to show the toast.
|
||||||
|
const [toastMessage, setToastMessage] = useState(/** @type{null|string} */ (null));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.hash === '#export-tags') {
|
||||||
|
setToastMessage(intl.formatMessage(messages.exportTagsCreatingToastMessage));
|
||||||
|
getTagsExportFile(courseId, courseName).then(() => {
|
||||||
|
setToastMessage(intl.formatMessage(messages.exportTagsSuccessToastMessage));
|
||||||
|
}).catch(() => {
|
||||||
|
setToastMessage(intl.formatMessage(messages.exportTagsErrorToastMessage));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete `#export-tags` from location
|
||||||
|
window.location.href = '#';
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
const [sections, setSections] = useState(sectionsList);
|
const [sections, setSections] = useState(sectionsList);
|
||||||
|
|
||||||
const restoreSectionList = () => {
|
const restoreSectionList = () => {
|
||||||
@@ -438,6 +459,7 @@ const CourseOutline = ({ courseId }) => {
|
|||||||
onConfigureSubmit={handleConfigureItemSubmit}
|
onConfigureSubmit={handleConfigureItemSubmit}
|
||||||
currentItemData={currentItemData}
|
currentItemData={currentItemData}
|
||||||
enableProctoredExams={enableProctoredExams}
|
enableProctoredExams={enableProctoredExams}
|
||||||
|
isSelfPaced={statusBarData.isSelfPaced}
|
||||||
/>
|
/>
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
category={deleteCategory}
|
category={deleteCategory}
|
||||||
@@ -457,6 +479,15 @@ const CourseOutline = ({ courseId }) => {
|
|||||||
onInternetConnectionFailed={handleInternetConnectionFailed}
|
onInternetConnectionFailed={handleInternetConnectionFailed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{toastMessage && (
|
||||||
|
<Toast
|
||||||
|
show
|
||||||
|
onClose={/* istanbul ignore next */ () => setToastMessage(null)}
|
||||||
|
data-testid="taxonomy-toast"
|
||||||
|
>
|
||||||
|
{toastMessage}
|
||||||
|
</Toast>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
act, render, waitFor, fireEvent, within,
|
act, render, waitFor, fireEvent, within, screen,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { AppProvider } from '@edx/frontend-platform/react';
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
@@ -10,6 +10,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { closestCorners } from '@dnd-kit/core';
|
import { closestCorners } from '@dnd-kit/core';
|
||||||
|
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
getCourseBestPracticesApiUrl,
|
getCourseBestPracticesApiUrl,
|
||||||
getCourseLaunchApiUrl,
|
getCourseLaunchApiUrl,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
getCourseBlockApiUrl,
|
getCourseBlockApiUrl,
|
||||||
getCourseItemApiUrl,
|
getCourseItemApiUrl,
|
||||||
getXBlockBaseApiUrl,
|
getXBlockBaseApiUrl,
|
||||||
|
exportTags,
|
||||||
} from './data/api';
|
} from './data/api';
|
||||||
import { RequestStatus } from '../data/constants';
|
import { RequestStatus } from '../data/constants';
|
||||||
import {
|
import {
|
||||||
@@ -74,9 +76,7 @@ global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
|||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
useLocation: () => ({
|
useLocation: jest.fn(),
|
||||||
pathname: mockPathname,
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../help-urls/hooks', () => ({
|
jest.mock('../help-urls/hooks', () => ({
|
||||||
@@ -135,6 +135,10 @@ describe('<CourseOutline />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useLocation.mockReturnValue({
|
||||||
|
pathname: mockPathname,
|
||||||
|
});
|
||||||
|
|
||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
axiosMock
|
axiosMock
|
||||||
@@ -1405,6 +1409,7 @@ describe('<CourseOutline />', () => {
|
|||||||
publish: 'republish',
|
publish: 'republish',
|
||||||
metadata: {
|
metadata: {
|
||||||
visible_to_staff_only: isVisibleToStaffOnly,
|
visible_to_staff_only: isVisibleToStaffOnly,
|
||||||
|
discussion_enabled: false,
|
||||||
group_access: newGroupAccess,
|
group_access: newGroupAccess,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1423,6 +1428,7 @@ describe('<CourseOutline />', () => {
|
|||||||
|
|
||||||
// after configuraiton response
|
// after configuraiton response
|
||||||
unit.visibilityState = 'staff_only';
|
unit.visibilityState = 'staff_only';
|
||||||
|
unit.discussion_enabled = false;
|
||||||
unit.userPartitionInfo = {
|
unit.userPartitionInfo = {
|
||||||
selectablePartitions: [
|
selectablePartitions: [
|
||||||
{
|
{
|
||||||
@@ -1465,6 +1471,11 @@ describe('<CourseOutline />', () => {
|
|||||||
)).toBeInTheDocument();
|
)).toBeInTheDocument();
|
||||||
let visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
|
let visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
|
||||||
await act(async () => fireEvent.click(visibilityCheckbox));
|
await act(async () => fireEvent.click(visibilityCheckbox));
|
||||||
|
let discussionCheckbox = await within(configureModal).findByLabelText(
|
||||||
|
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
|
||||||
|
);
|
||||||
|
expect(discussionCheckbox).toBeChecked();
|
||||||
|
await act(async () => fireEvent.click(discussionCheckbox));
|
||||||
|
|
||||||
let groupeType = await within(configureModal).findByTestId('group-type-select');
|
let groupeType = await within(configureModal).findByTestId('group-type-select');
|
||||||
fireEvent.change(groupeType, { target: { value: '0' } });
|
fireEvent.change(groupeType, { target: { value: '0' } });
|
||||||
@@ -1481,6 +1492,10 @@ describe('<CourseOutline />', () => {
|
|||||||
configureModal = await findByTestId('configure-modal');
|
configureModal = await findByTestId('configure-modal');
|
||||||
visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
|
visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
|
||||||
expect(visibilityCheckbox).toBeChecked();
|
expect(visibilityCheckbox).toBeChecked();
|
||||||
|
discussionCheckbox = await within(configureModal).findByLabelText(
|
||||||
|
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
|
||||||
|
);
|
||||||
|
expect(discussionCheckbox).not.toBeChecked();
|
||||||
|
|
||||||
groupeType = await within(configureModal).findByTestId('group-type-select');
|
groupeType = await within(configureModal).findByTestId('group-type-select');
|
||||||
expect(groupeType).toHaveValue('0');
|
expect(groupeType).toHaveValue('0');
|
||||||
@@ -2237,4 +2252,38 @@ describe('<CourseOutline />', () => {
|
|||||||
// check pasteFileNotices in store
|
// check pasteFileNotices in store
|
||||||
expect(store.getState().courseOutline.pasteFileNotices).toEqual({});
|
expect(store.getState().courseOutline.pasteFileNotices).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show toats on export tags', async () => {
|
||||||
|
const expectedResponse = 'this is a test';
|
||||||
|
axiosMock
|
||||||
|
.onGet(exportTags(courseId))
|
||||||
|
.reply(200, expectedResponse);
|
||||||
|
useLocation.mockReturnValue({
|
||||||
|
pathname: '/foo-bar',
|
||||||
|
hash: '#export-tags',
|
||||||
|
});
|
||||||
|
window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo');
|
||||||
|
window.URL.revokeObjectURL = jest.fn();
|
||||||
|
render(<RootWrapper />);
|
||||||
|
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId));
|
||||||
|
expect(expectedRequest.length).toBe(1);
|
||||||
|
|
||||||
|
expect(await screen.findByText('Course tags exported successfully')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show toast on export tags error', async () => {
|
||||||
|
axiosMock
|
||||||
|
.onGet(exportTags(courseId))
|
||||||
|
.reply(404);
|
||||||
|
useLocation.mockReturnValue({
|
||||||
|
pathname: '/foo-bar',
|
||||||
|
hash: '#export-tags',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RootWrapper />);
|
||||||
|
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
|
|||||||
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
|
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
|
||||||
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
|
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
|
||||||
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
|
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
|
||||||
|
export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} courseOutline
|
* @typedef {Object} courseOutline
|
||||||
@@ -310,7 +311,7 @@ export async function configureCourseSubsection(
|
|||||||
* @param {object} groupAccess
|
* @param {object} groupAccess
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAccess) {
|
export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAccess, discussionEnabled) {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.post(getCourseItemApiUrl(unitId), {
|
.post(getCourseItemApiUrl(unitId), {
|
||||||
publish: 'republish',
|
publish: 'republish',
|
||||||
@@ -318,6 +319,7 @@ export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAcc
|
|||||||
// The backend expects metadata.visible_to_staff_only to either true or null
|
// The backend expects metadata.visible_to_staff_only to either true or null
|
||||||
visible_to_staff_only: isVisibleToStaffOnly ? true : null,
|
visible_to_staff_only: isVisibleToStaffOnly ? true : null,
|
||||||
group_access: groupAccess,
|
group_access: groupAccess,
|
||||||
|
discussion_enabled: discussionEnabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -458,3 +460,33 @@ export async function dismissNotification(url) {
|
|||||||
await getAuthenticatedHttpClient()
|
await getAuthenticatedHttpClient()
|
||||||
.delete(url);
|
.delete(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the file of the exported tags
|
||||||
|
* @param {string} courseId The ID of the content
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export async function getTagsExportFile(courseId, courseName) {
|
||||||
|
// Gets exported tags and builds the blob to download CSV file.
|
||||||
|
// This can be done with this code:
|
||||||
|
// `window.location.href = exportTags(contentId);`
|
||||||
|
// but it is done in this way so we know when the operation ends to close the toast.
|
||||||
|
const response = await getAuthenticatedHttpClient().get(exportTags(courseId), {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw response.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([response.data], { type: 'text/csv' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${courseName}.csv`;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|||||||
@@ -334,11 +334,11 @@ export function configureCourseSubsectionQuery(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureCourseUnitQuery(itemId, sectionId, isVisibleToStaffOnly, groupAccess) {
|
export function configureCourseUnitQuery(itemId, sectionId, isVisibleToStaffOnly, groupAccess, discussionEnabled) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(configureCourseItemQuery(
|
dispatch(configureCourseItemQuery(
|
||||||
sectionId,
|
sectionId,
|
||||||
async () => configureCourseUnit(itemId, isVisibleToStaffOnly, groupAccess),
|
async () => configureCourseUnit(itemId, isVisibleToStaffOnly, groupAccess, discussionEnabled),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const HighlightsModal = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
hasCloseButton
|
hasCloseButton
|
||||||
isFullscreenOnMobile
|
isFullscreenOnMobile
|
||||||
|
isOverflowVisible={false}
|
||||||
>
|
>
|
||||||
<ModalDialog.Header className="highlights-modal__header">
|
<ModalDialog.Header className="highlights-modal__header">
|
||||||
<ModalDialog.Title>
|
<ModalDialog.Title>
|
||||||
|
|||||||
@@ -29,6 +29,21 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.course-outline.section-list.button.new-section',
|
id: 'course-authoring.course-outline.section-list.button.new-section',
|
||||||
defaultMessage: 'New section',
|
defaultMessage: 'New section',
|
||||||
},
|
},
|
||||||
|
exportTagsCreatingToastMessage: {
|
||||||
|
id: 'course-authoring.course-outline.export-tags.toast.creating.message',
|
||||||
|
defaultMessage: 'Please wait. Creating export file for course tags...',
|
||||||
|
description: 'In progress message in toast when exporting tags of a course',
|
||||||
|
},
|
||||||
|
exportTagsSuccessToastMessage: {
|
||||||
|
id: 'course-authoring.course-outline.export-tags.toast.success.message',
|
||||||
|
defaultMessage: 'Course tags exported successfully',
|
||||||
|
description: 'Success message in toast when exporting tags of a course',
|
||||||
|
},
|
||||||
|
exportTagsErrorToastMessage: {
|
||||||
|
id: 'course-authoring.course-outline.export-tags.toast.error.message',
|
||||||
|
defaultMessage: 'An error has occurred creating the file',
|
||||||
|
description: 'Error message in toast when exporting tags of a course',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const PublishModal = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
hasCloseButton
|
hasCloseButton
|
||||||
isFullscreenOnMobile
|
isFullscreenOnMobile
|
||||||
|
isOverflowVisible={false}
|
||||||
>
|
>
|
||||||
<ModalDialog.Header className="publish-modal__header">
|
<ModalDialog.Header className="publish-modal__header">
|
||||||
<ModalDialog.Title>
|
<ModalDialog.Title>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useToggle } from '@openedx/paragon';
|
import { useToggle } from '@openedx/paragon';
|
||||||
|
|
||||||
import { COMMA_SEPARATED_DATE_FORMAT } from '../constants';
|
import { COMMA_SEPARATED_DATE_FORMAT } from '../constants';
|
||||||
|
import { convertToDateFromString } from '../utils';
|
||||||
import { getCourseHandouts, getCourseUpdates } from './data/selectors';
|
import { getCourseHandouts, getCourseUpdates } from './data/selectors';
|
||||||
import { REQUEST_TYPES } from './constants';
|
import { REQUEST_TYPES } from './constants';
|
||||||
import {
|
import {
|
||||||
@@ -55,9 +56,10 @@ const useCourseUpdates = ({ courseId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatesSubmit = (data) => {
|
const handleUpdatesSubmit = (data) => {
|
||||||
|
const dateWithoutTimezone = convertToDateFromString(data.date);
|
||||||
const dataToSend = {
|
const dataToSend = {
|
||||||
...data,
|
...data,
|
||||||
date: moment(data.date).format(COMMA_SEPARATED_DATE_FORMAT),
|
date: moment(dateWithoutTimezone).format(COMMA_SEPARATED_DATE_FORMAT),
|
||||||
};
|
};
|
||||||
const { id, date, content } = dataToSend;
|
const { id, date, content } = dataToSend;
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
act,
|
act,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
import moment from 'moment/moment';
|
import moment from 'moment/moment';
|
||||||
|
|
||||||
|
import initializeStore from '../../store';
|
||||||
import { REQUEST_TYPES } from '../constants';
|
import { REQUEST_TYPES } from '../constants';
|
||||||
import { courseHandoutsMock, courseUpdatesMock } from '../__mocks__';
|
import { courseHandoutsMock, courseUpdatesMock } from '../__mocks__';
|
||||||
import UpdateForm from './UpdateForm';
|
import UpdateForm from './UpdateForm';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
|
let store;
|
||||||
const closeMock = jest.fn();
|
const closeMock = jest.fn();
|
||||||
const onSubmitMock = jest.fn();
|
const onSubmitMock = jest.fn();
|
||||||
const addNewUpdateMock = { id: 0, date: moment().utc().toDate(), content: 'Some content' };
|
const addNewUpdateMock = { id: 0, date: moment().utc().toDate(), content: 'Some content' };
|
||||||
@@ -48,18 +52,32 @@ const courseUpdatesInitialValues = (requestType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderComponent = ({ requestType }) => render(
|
const renderComponent = ({ requestType }) => render(
|
||||||
<IntlProvider locale="en">
|
<AppProvider store={store}>
|
||||||
<UpdateForm
|
<IntlProvider locale="en">
|
||||||
isOpen
|
<UpdateForm
|
||||||
close={closeMock}
|
isOpen
|
||||||
requestType={requestType}
|
close={closeMock}
|
||||||
onSubmit={onSubmitMock}
|
requestType={requestType}
|
||||||
courseUpdatesInitialValues={courseUpdatesInitialValues(requestType)}
|
onSubmit={onSubmitMock}
|
||||||
/>
|
courseUpdatesInitialValues={courseUpdatesInitialValues(requestType)}
|
||||||
</IntlProvider>,
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('<UpdateForm />', () => {
|
describe('<UpdateForm />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store = initializeStore();
|
||||||
|
});
|
||||||
it('render Add new update form correctly', async () => {
|
it('render Add new update form correctly', async () => {
|
||||||
const { getByText, getByDisplayValue, getByRole } = renderComponent({ requestType: REQUEST_TYPES.add_new_update });
|
const { getByText, getByDisplayValue, getByRole } = renderComponent({ requestType: REQUEST_TYPES.add_new_update });
|
||||||
const { date } = courseUpdatesInitialValues(REQUEST_TYPES.add_new_update);
|
const { date } = courseUpdatesInitialValues(REQUEST_TYPES.add_new_update);
|
||||||
|
|||||||
@@ -53,3 +53,7 @@ export const VisibilityTypes = /** @type {const} */ ({
|
|||||||
UNSCHEDULED: 'unscheduled',
|
UNSCHEDULED: 'unscheduled',
|
||||||
NEEDS_ATTENTION: 'needs_attention',
|
NEEDS_ATTENTION: 'needs_attention',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const TOTAL_LENGTH_KEY = 'total-length';
|
||||||
|
|
||||||
|
export const MAX_TOTAL_LENGTH = 65;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const FileValidationModal = ({
|
|||||||
title={intl.formatMessage(messages.overwriteModalTitle)}
|
title={intl.formatMessage(messages.overwriteModalTitle)}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
|
isOverflowVisible={false}
|
||||||
>
|
>
|
||||||
<ModalDialog.Header>
|
<ModalDialog.Header>
|
||||||
<ModalDialog.Title>
|
<ModalDialog.Title>
|
||||||
|
|||||||
@@ -165,12 +165,13 @@ export function updateAssetLock({ assetId, courseId, locked }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateLockStatus({ assetId, courseId, locked });
|
await updateLockStatus({ assetId, courseId, locked });
|
||||||
|
const lockStatus = locked ? 'locked' : 'public';
|
||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
modelType: 'assets',
|
modelType: 'assets',
|
||||||
model: {
|
model: {
|
||||||
id: assetId,
|
id: assetId,
|
||||||
locked,
|
locked,
|
||||||
lockStatus: locked,
|
lockStatus,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.SUCCESSFUL }));
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ const messages = defineMessages({
|
|||||||
description: 'Label for lock file checkbox in info modal',
|
description: 'Label for lock file checkbox in info modal',
|
||||||
},
|
},
|
||||||
activeCheckboxLabel: {
|
activeCheckboxLabel: {
|
||||||
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.activeCheckbox.label',
|
id: 'course-authoring.files-and-videos.file-info.activeCheckbox.label',
|
||||||
defaultMessage: 'Active',
|
defaultMessage: 'Active',
|
||||||
description: 'Label for active checkbox in filter section of sort and filter modal',
|
description: 'Label for active checkbox in filter section of sort and filter modal',
|
||||||
},
|
},
|
||||||
inactiveCheckboxLabel: {
|
inactiveCheckboxLabel: {
|
||||||
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.inactiveCheckbox.label',
|
id: 'course-authoring.files-and-videos.file-info.inactiveCheckbox.label',
|
||||||
defaultMessage: 'Inactive',
|
defaultMessage: 'Inactive',
|
||||||
description: 'Label for inactive checkbox in filter section of sort and filter modal',
|
description: 'Label for inactive checkbox in filter section of sort and filter modal',
|
||||||
},
|
},
|
||||||
@@ -111,6 +111,15 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Cancel',
|
defaultMessage: 'Cancel',
|
||||||
description: 'The message displayed in the button to confirm cancelling the upload',
|
description: 'The message displayed in the button to confirm cancelling the upload',
|
||||||
},
|
},
|
||||||
|
lockFileTooltipContent: {
|
||||||
|
id: 'course-authoring.files-and-uploads.file-info.lockFile.tooltip.content',
|
||||||
|
defaultMessage: `By default, anyone can access a file you upload if
|
||||||
|
they know the web URL, even if they are not enrolled in your course.
|
||||||
|
You can prevent outside access to a file by locking the file. When
|
||||||
|
you lock a file, the web URL only allows learners who are enrolled
|
||||||
|
in your course and signed in to access the file.`,
|
||||||
|
description: 'Tooltip message for the lock icon in the table view of files',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect, Provider } from 'react-redux';
|
import { connect, Provider, useSelector } from 'react-redux';
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import {
|
import {
|
||||||
@@ -18,20 +18,19 @@ export const SUPPORTED_TEXT_EDITORS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = () => ({
|
const mapStateToProps = () => ({
|
||||||
assets: {},
|
images: {},
|
||||||
lmsEndpointUrl: getConfig().LMS_BASE_URL,
|
lmsEndpointUrl: getConfig().LMS_BASE_URL,
|
||||||
studioEndpointUrl: getConfig().STUDIO_BASE_URL,
|
studioEndpointUrl: getConfig().STUDIO_BASE_URL,
|
||||||
isLibrary: true,
|
isLibrary: true,
|
||||||
onEditorChange: () => ({}),
|
onEditorChange: () => ({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const Editor = connect(mapStateToProps)(TinyMceWidget);
|
const Editor = connect(mapStateToProps)(TinyMceWidget);
|
||||||
|
|
||||||
export const WysiwygEditor = ({
|
export const WysiwygEditor = ({
|
||||||
initialValue, editorType, onChange, minHeight,
|
initialValue, editorType, onChange, minHeight,
|
||||||
}) => {
|
}) => {
|
||||||
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
|
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
|
||||||
|
const { courseId } = useSelector((state) => state.courseDetail);
|
||||||
const isEquivalentCodeExtraSpaces = (first, second) => {
|
const isEquivalentCodeExtraSpaces = (first, second) => {
|
||||||
// Utils allows to compare code extra spaces
|
// Utils allows to compare code extra spaces
|
||||||
const removeWhitespace = (str) => str.replace(/\s/g, '');
|
const removeWhitespace = (str) => str.replace(/\s/g, '');
|
||||||
@@ -75,6 +74,7 @@ export const WysiwygEditor = ({
|
|||||||
setEditorRef={setEditorRef}
|
setEditorRef={setEditorRef}
|
||||||
onChange={handleUpdate}
|
onChange={handleUpdate}
|
||||||
initializeEditor={() => ({})}
|
initializeEditor={() => ({})}
|
||||||
|
learningContextId={courseId}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const BasicTab = ({
|
|||||||
setFieldValue,
|
setFieldValue,
|
||||||
courseGraders,
|
courseGraders,
|
||||||
isSubsection,
|
isSubsection,
|
||||||
|
isSelfPaced,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -27,26 +28,30 @@ const BasicTab = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.releaseDateAndTime} /></h5>
|
{!isSelfPaced && (
|
||||||
<hr />
|
<>
|
||||||
<div data-testid="release-date-stack">
|
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.releaseDateAndTime} /></h5>
|
||||||
<Stack className="mt-3" direction="horizontal" gap={5}>
|
<hr />
|
||||||
<DatepickerControl
|
<div data-testid="release-date-stack">
|
||||||
type={DATEPICKER_TYPES.date}
|
<Stack className="mt-3" direction="horizontal" gap={5}>
|
||||||
value={releaseDate}
|
<DatepickerControl
|
||||||
label={intl.formatMessage(messages.releaseDate)}
|
type={DATEPICKER_TYPES.date}
|
||||||
controlName="state-date"
|
value={releaseDate}
|
||||||
onChange={(val) => setFieldValue('releaseDate', val)}
|
label={intl.formatMessage(messages.releaseDate)}
|
||||||
/>
|
controlName="state-date"
|
||||||
<DatepickerControl
|
onChange={(val) => setFieldValue('releaseDate', val)}
|
||||||
type={DATEPICKER_TYPES.time}
|
/>
|
||||||
value={releaseDate}
|
<DatepickerControl
|
||||||
label={intl.formatMessage(messages.releaseTimeUTC)}
|
type={DATEPICKER_TYPES.time}
|
||||||
controlName="start-time"
|
value={releaseDate}
|
||||||
onChange={(val) => setFieldValue('releaseDate', val)}
|
label={intl.formatMessage(messages.releaseTimeUTC)}
|
||||||
/>
|
controlName="start-time"
|
||||||
</Stack>
|
onChange={(val) => setFieldValue('releaseDate', val)}
|
||||||
</div>
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
isSubsection && (
|
isSubsection && (
|
||||||
<div>
|
<div>
|
||||||
@@ -66,25 +71,27 @@ const BasicTab = ({
|
|||||||
{createOptions()}
|
{createOptions()}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<div data-testid="due-date-stack">
|
{!isSelfPaced && (
|
||||||
<Stack className="mt-3" direction="horizontal" gap={5}>
|
<div data-testid="due-date-stack">
|
||||||
<DatepickerControl
|
<Stack className="mt-3" direction="horizontal" gap={5}>
|
||||||
type={DATEPICKER_TYPES.date}
|
<DatepickerControl
|
||||||
value={dueDate}
|
type={DATEPICKER_TYPES.date}
|
||||||
label={intl.formatMessage(messages.dueDate)}
|
value={dueDate}
|
||||||
controlName="state-date"
|
label={intl.formatMessage(messages.dueDate)}
|
||||||
onChange={(val) => setFieldValue('dueDate', val)}
|
controlName="state-date"
|
||||||
data-testid="due-date-picker"
|
onChange={(val) => setFieldValue('dueDate', val)}
|
||||||
/>
|
data-testid="due-date-picker"
|
||||||
<DatepickerControl
|
/>
|
||||||
type={DATEPICKER_TYPES.time}
|
<DatepickerControl
|
||||||
value={dueDate}
|
type={DATEPICKER_TYPES.time}
|
||||||
label={intl.formatMessage(messages.dueTimeUTC)}
|
value={dueDate}
|
||||||
controlName="start-time"
|
label={intl.formatMessage(messages.dueTimeUTC)}
|
||||||
onChange={(val) => setFieldValue('dueDate', val)}
|
controlName="start-time"
|
||||||
/>
|
onChange={(val) => setFieldValue('dueDate', val)}
|
||||||
</Stack>
|
/>
|
||||||
</div>
|
</Stack>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -101,6 +108,7 @@ BasicTab.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
courseGraders: PropTypes.arrayOf(PropTypes.string).isRequired,
|
courseGraders: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
setFieldValue: PropTypes.func.isRequired,
|
setFieldValue: PropTypes.func.isRequired,
|
||||||
|
isSelfPaced: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(BasicTab);
|
export default injectIntl(BasicTab);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const ConfigureModal = ({
|
|||||||
currentItemData,
|
currentItemData,
|
||||||
enableProctoredExams,
|
enableProctoredExams,
|
||||||
isXBlockComponent,
|
isXBlockComponent,
|
||||||
|
isSelfPaced,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
@@ -58,6 +59,7 @@ const ConfigureModal = ({
|
|||||||
supportsOnboarding,
|
supportsOnboarding,
|
||||||
showReviewRules,
|
showReviewRules,
|
||||||
onlineProctoringRules,
|
onlineProctoringRules,
|
||||||
|
discussionEnabled,
|
||||||
} = currentItemData;
|
} = currentItemData;
|
||||||
|
|
||||||
const getSelectedGroups = () => {
|
const getSelectedGroups = () => {
|
||||||
@@ -98,6 +100,7 @@ const ConfigureModal = ({
|
|||||||
// by default it is -1 i.e. accessible to all learners & staff
|
// by default it is -1 i.e. accessible to all learners & staff
|
||||||
selectedPartitionIndex: userPartitionInfo?.selectedPartitionIndex,
|
selectedPartitionIndex: userPartitionInfo?.selectedPartitionIndex,
|
||||||
selectedGroups: getSelectedGroups(),
|
selectedGroups: getSelectedGroups(),
|
||||||
|
discussionEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
@@ -127,6 +130,7 @@ const ConfigureModal = ({
|
|||||||
).nullable(true),
|
).nullable(true),
|
||||||
selectedPartitionIndex: Yup.number().integer(),
|
selectedPartitionIndex: Yup.number().integer(),
|
||||||
selectedGroups: Yup.array().of(Yup.string()),
|
selectedGroups: Yup.array().of(Yup.string()),
|
||||||
|
discussionEnabled: Yup.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
|
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
|
||||||
@@ -168,7 +172,7 @@ const ConfigureModal = ({
|
|||||||
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
|
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
|
||||||
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
|
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
|
||||||
}
|
}
|
||||||
onConfigureSubmit(data.isVisibleToStaffOnly, groupAccess);
|
onConfigureSubmit(data.isVisibleToStaffOnly, groupAccess, data.discussionEnabled);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -186,6 +190,7 @@ const ConfigureModal = ({
|
|||||||
setFieldValue={setFieldValue}
|
setFieldValue={setFieldValue}
|
||||||
isSubsection={isSubsection}
|
isSubsection={isSubsection}
|
||||||
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
||||||
|
isSelfPaced={isSelfPaced}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
||||||
@@ -208,6 +213,7 @@ const ConfigureModal = ({
|
|||||||
setFieldValue={setFieldValue}
|
setFieldValue={setFieldValue}
|
||||||
isSubsection={isSubsection}
|
isSubsection={isSubsection}
|
||||||
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
||||||
|
isSelfPaced={isSelfPaced}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
||||||
@@ -259,6 +265,7 @@ const ConfigureModal = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
hasCloseButton
|
hasCloseButton
|
||||||
isFullscreenOnMobile
|
isFullscreenOnMobile
|
||||||
|
isOverflowVisible={false}
|
||||||
>
|
>
|
||||||
<div data-testid="configure-modal">
|
<div data-testid="configure-modal">
|
||||||
<ModalDialog.Header className="configure-modal__header">
|
<ModalDialog.Header className="configure-modal__header">
|
||||||
@@ -358,8 +365,10 @@ ConfigureModal.propTypes = {
|
|||||||
supportsOnboarding: PropTypes.bool,
|
supportsOnboarding: PropTypes.bool,
|
||||||
showReviewRules: PropTypes.bool,
|
showReviewRules: PropTypes.bool,
|
||||||
onlineProctoringRules: PropTypes.string,
|
onlineProctoringRules: PropTypes.string,
|
||||||
|
discussionEnabled: PropTypes.bool.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
isXBlockComponent: PropTypes.bool,
|
isXBlockComponent: PropTypes.bool,
|
||||||
|
isSelfPaced: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConfigureModal;
|
export default ConfigureModal;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const renderComponent = () => render(
|
|||||||
onClose={onCloseMock}
|
onClose={onCloseMock}
|
||||||
onConfigureSubmit={onConfigureSubmitMock}
|
onConfigureSubmit={onConfigureSubmitMock}
|
||||||
currentItemData={currentSectionMock}
|
currentItemData={currentSectionMock}
|
||||||
|
isSelfPaced={false}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
</AppProvider>,
|
</AppProvider>,
|
||||||
@@ -85,7 +86,7 @@ describe('<ConfigureModal /> for Section', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderSubsectionComponent = () => render(
|
const renderSubsectionComponent = (props) => render(
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<ConfigureModal
|
<ConfigureModal
|
||||||
@@ -93,6 +94,8 @@ const renderSubsectionComponent = () => render(
|
|||||||
onClose={onCloseMock}
|
onClose={onCloseMock}
|
||||||
onConfigureSubmit={onConfigureSubmitMock}
|
onConfigureSubmit={onConfigureSubmitMock}
|
||||||
currentItemData={currentSubsectionMock}
|
currentItemData={currentSubsectionMock}
|
||||||
|
isSelfPaced={false}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
</AppProvider>,
|
</AppProvider>,
|
||||||
@@ -129,6 +132,14 @@ describe('<ConfigureModal /> for Subsection', () => {
|
|||||||
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides release and due dates for self paced courses', () => {
|
||||||
|
const { queryByText } = renderSubsectionComponent({ isSelfPaced: true });
|
||||||
|
expect(queryByText(messages.releaseDate.defaultMessage)).not.toBeInTheDocument();
|
||||||
|
expect(queryByText(messages.releaseTimeUTC.defaultMessage)).not.toBeInTheDocument();
|
||||||
|
expect(queryByText(messages.dueDate.defaultMessage)).not.toBeInTheDocument();
|
||||||
|
expect(queryByText(messages.dueTimeUTC.defaultMessage)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('switches to the subsection Visibility tab and renders correctly', () => {
|
it('switches to the subsection Visibility tab and renders correctly', () => {
|
||||||
const { getByRole, getByText } = renderSubsectionComponent();
|
const { getByRole, getByText } = renderSubsectionComponent();
|
||||||
|
|
||||||
@@ -198,6 +209,7 @@ describe('<ConfigureModal /> for Unit', () => {
|
|||||||
expect(getByText(`${currentUnitMock.displayName} settings`)).toBeInTheDocument();
|
expect(getByText(`${currentUnitMock.displayName} settings`)).toBeInTheDocument();
|
||||||
expect(getByText(messages.unitVisibility.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.unitVisibility.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.discussionEnabledCheckbox.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,17 @@ const UnitTab = ({
|
|||||||
isVisibleToStaffOnly,
|
isVisibleToStaffOnly,
|
||||||
selectedPartitionIndex,
|
selectedPartitionIndex,
|
||||||
selectedGroups,
|
selectedGroups,
|
||||||
|
discussionEnabled,
|
||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleVisibilityChange = (e) => {
|
||||||
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDiscussionChange = (e) => {
|
||||||
|
setFieldValue('discussionEnabled', e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelect = (e) => {
|
const handleSelect = (e) => {
|
||||||
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
|
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
|
||||||
setFieldValue('selectedGroups', []);
|
setFieldValue('selectedGroups', []);
|
||||||
@@ -42,9 +47,9 @@ const UnitTab = ({
|
|||||||
<>
|
<>
|
||||||
{!isXBlockComponent && (
|
{!isXBlockComponent && (
|
||||||
<>
|
<>
|
||||||
<h3 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h3>
|
<h4 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h4>
|
||||||
<hr />
|
<hr />
|
||||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="unit-visibility-checkbox">
|
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleVisibilityChange} data-testid="unit-visibility-checkbox">
|
||||||
<FormattedMessage {...messages.hideFromLearners} />
|
<FormattedMessage {...messages.hideFromLearners} />
|
||||||
</Form.Checkbox>
|
</Form.Checkbox>
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
@@ -52,77 +57,84 @@ const UnitTab = ({
|
|||||||
<FormattedMessage {...messages.unitVisibilityWarning} />
|
<FormattedMessage {...messages.unitVisibilityWarning} />
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<hr />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Form.Group controlId="groupSelect">
|
{userPartitionInfo.selectablePartitions.length > 0 && (
|
||||||
<Form.Label as="legend" className="font-weight-bold">
|
<Form.Group controlId="groupSelect">
|
||||||
<FormattedMessage {...messages.restrictAccessTo} />
|
<h4 className="mt-3"><FormattedMessage {...messages.unitAccess} /></h4>
|
||||||
</Form.Label>
|
<hr />
|
||||||
<Form.Control
|
<Form.Label as="legend" className="font-weight-bold">
|
||||||
as="select"
|
<FormattedMessage {...messages.restrictAccessTo} />
|
||||||
name="groupSelect"
|
</Form.Label>
|
||||||
value={selectedPartitionIndex}
|
<Form.Control
|
||||||
onChange={handleSelect}
|
as="select"
|
||||||
data-testid="group-type-select"
|
name="groupSelect"
|
||||||
>
|
value={selectedPartitionIndex}
|
||||||
<option value="-1" key="-1">
|
onChange={handleSelect}
|
||||||
{userPartitionInfo.selectedPartitionIndex === -1
|
data-testid="group-type-select"
|
||||||
? intl.formatMessage(messages.unitSelectGroupType)
|
>
|
||||||
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
|
<option value="-1" key="-1">
|
||||||
</option>
|
{userPartitionInfo.selectedPartitionIndex === -1
|
||||||
{userPartitionInfo.selectablePartitions.map((partition, index) => (
|
? intl.formatMessage(messages.unitSelectGroupType)
|
||||||
<option
|
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
|
||||||
key={partition.id}
|
|
||||||
value={index}
|
|
||||||
>
|
|
||||||
{partition.name}
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
{userPartitionInfo.selectablePartitions.map((partition, index) => (
|
||||||
</Form.Control>
|
<option
|
||||||
|
key={partition.id}
|
||||||
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
|
value={index}
|
||||||
<Form.Group controlId="select-groups-checkboxes">
|
>
|
||||||
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
|
{partition.name}
|
||||||
<div
|
</option>
|
||||||
role="group"
|
))}
|
||||||
className="d-flex flex-column"
|
</Form.Control>
|
||||||
data-testid="group-checkboxes"
|
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
|
||||||
aria-labelledby="select-groups-checkboxes"
|
<Form.Group controlId="select-groups-checkboxes">
|
||||||
>
|
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
|
||||||
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (
|
<div
|
||||||
<Form.Group
|
role="group"
|
||||||
key={group.id}
|
className="d-flex flex-column"
|
||||||
className="pgn__form-checkbox"
|
data-testid="group-checkboxes"
|
||||||
>
|
aria-labelledby="select-groups-checkboxes"
|
||||||
<Field
|
>
|
||||||
as={Form.Control}
|
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (
|
||||||
className="flex-grow-0 mr-1"
|
<Form.Group
|
||||||
controlClassName="pgn__form-checkbox-input mr-1"
|
key={group.id}
|
||||||
type="checkbox"
|
className="pgn__form-checkbox"
|
||||||
value={`${group.id}`}
|
>
|
||||||
name="selectedGroups"
|
<Field
|
||||||
/>
|
as={Form.Control}
|
||||||
<div>
|
className="flex-grow-0 mr-1"
|
||||||
<Form.Label
|
controlClassName="pgn__form-checkbox-input mr-1"
|
||||||
className={classNames({ 'text-danger': checkIsDeletedGroup(group) })}
|
type="checkbox"
|
||||||
isInline
|
value={`${group.id}`}
|
||||||
>
|
name="selectedGroups"
|
||||||
{group.name}
|
/>
|
||||||
</Form.Label>
|
<div>
|
||||||
{group.deleted && (
|
<Form.Label
|
||||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
className={classNames({ 'text-danger': checkIsDeletedGroup(group) })}
|
||||||
{intl.formatMessage(messages.unitSelectDeletedGroupErrorMessage)}
|
isInline
|
||||||
</Form.Control.Feedback>
|
>
|
||||||
)}
|
{group.name}
|
||||||
</div>
|
</Form.Label>
|
||||||
</Form.Group>
|
{group.deleted && (
|
||||||
))}
|
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||||
</div>
|
{intl.formatMessage(messages.unitSelectDeletedGroupErrorMessage)}
|
||||||
</Form.Group>
|
</Form.Control.Feedback>
|
||||||
)}
|
)}
|
||||||
</Form.Group>
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
<h4 className="mt-4"><FormattedMessage {...messages.discussionEnabledSectionTitle} /></h4>
|
||||||
|
<hr />
|
||||||
|
<Form.Checkbox checked={discussionEnabled} onChange={handleDiscussionChange}>
|
||||||
|
<FormattedMessage {...messages.discussionEnabledCheckbox} />
|
||||||
|
</Form.Checkbox>
|
||||||
|
<p className="x-small font-weight-bold"><FormattedMessage {...messages.discussionEnabledDescription} /></p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -135,6 +147,7 @@ UnitTab.propTypes = {
|
|||||||
isXBlockComponent: PropTypes.bool,
|
isXBlockComponent: PropTypes.bool,
|
||||||
values: PropTypes.shape({
|
values: PropTypes.shape({
|
||||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||||
|
discussionEnabled: PropTypes.bool.isRequired,
|
||||||
selectedPartitionIndex: PropTypes.oneOfType([
|
selectedPartitionIndex: PropTypes.oneOfType([
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.number,
|
PropTypes.number,
|
||||||
|
|||||||
@@ -42,6 +42,22 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-visibility',
|
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-visibility',
|
||||||
defaultMessage: 'Unit visibility',
|
defaultMessage: 'Unit visibility',
|
||||||
},
|
},
|
||||||
|
unitAccess: {
|
||||||
|
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-access',
|
||||||
|
defaultMessage: 'Unit access',
|
||||||
|
},
|
||||||
|
discussionEnabledSectionTitle: {
|
||||||
|
id: 'course-authoring.course-outline.configure-modal.discussion-enabled.section-title',
|
||||||
|
defaultMessage: 'Discussion',
|
||||||
|
},
|
||||||
|
discussionEnabledCheckbox: {
|
||||||
|
id: 'course-authoring.course-outline.configure-modal.discussion-enabled.checkbox',
|
||||||
|
defaultMessage: 'Enable discussion',
|
||||||
|
},
|
||||||
|
discussionEnabledDescription: {
|
||||||
|
id: 'course-authoring.course-outline.configure-modal.discussion-enabled.description',
|
||||||
|
defaultMessage: 'Topics for unpublished units will not be created',
|
||||||
|
},
|
||||||
hideFromLearners: {
|
hideFromLearners: {
|
||||||
id: 'course-authoring.course-outline.configure-modal.visibility.hide-from-learners',
|
id: 'course-authoring.course-outline.configure-modal.visibility.hide-from-learners',
|
||||||
defaultMessage: 'Hide from learners',
|
defaultMessage: 'Hide from learners',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { TypeaheadDropdown } from '@edx/frontend-lib-content-components';
|
|||||||
|
|
||||||
import AlertMessage from '../alert-message';
|
import AlertMessage from '../alert-message';
|
||||||
import { STATEFUL_BUTTON_STATES } from '../../constants';
|
import { STATEFUL_BUTTON_STATES } from '../../constants';
|
||||||
import { RequestStatus } from '../../data/constants';
|
import { RequestStatus, TOTAL_LENGTH_KEY } from '../../data/constants';
|
||||||
import { getSavingStatus } from '../data/selectors';
|
import { getSavingStatus } from '../data/selectors';
|
||||||
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
||||||
import { updatePostErrors } from '../data/slice';
|
import { updatePostErrors } from '../data/slice';
|
||||||
@@ -132,6 +132,8 @@ const CreateOrRerunCourseForm = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const errorMessage = errors[TOTAL_LENGTH_KEY] || postErrors?.errMsg;
|
||||||
|
|
||||||
const createButtonState = {
|
const createButtonState = {
|
||||||
labels: {
|
labels: {
|
||||||
default: intl.formatMessage(isCreateNewCourse ? messages.createButton : messages.rerunCreateButton),
|
default: intl.formatMessage(isCreateNewCourse ? messages.createButton : messages.rerunCreateButton),
|
||||||
@@ -202,11 +204,11 @@ const CreateOrRerunCourseForm = ({
|
|||||||
return (
|
return (
|
||||||
<div className="create-or-rerun-course-form">
|
<div className="create-or-rerun-course-form">
|
||||||
<TransitionReplace>
|
<TransitionReplace>
|
||||||
{showErrorBanner ? (
|
{(errors[TOTAL_LENGTH_KEY] || showErrorBanner) ? (
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={InfoIcon}
|
icon={InfoIcon}
|
||||||
title={postErrors.errMsg}
|
title={errorMessage}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-labelledby={intl.formatMessage(
|
aria-labelledby={intl.formatMessage(
|
||||||
messages.alertErrorExistsAriaLabelledBy,
|
messages.alertErrorExistsAriaLabelledBy,
|
||||||
|
|||||||
@@ -198,6 +198,30 @@ describe('<CreateOrRerunCourseForm />', () => {
|
|||||||
expect(rerunBtn).toBeDisabled();
|
expect(rerunBtn).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows error message when total length exceeds 65 characters', async () => {
|
||||||
|
const updatedProps = {
|
||||||
|
...props,
|
||||||
|
initialValues: {
|
||||||
|
displayName: 'Long Title Course',
|
||||||
|
org: 'long-org',
|
||||||
|
number: 'number',
|
||||||
|
run: '2024',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<RootWrapper {...updatedProps} />);
|
||||||
|
await mockStore();
|
||||||
|
const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(numberInput, { target: { value: 'long-name-which-is-longer-than-65-characters-to-check-for-errors' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(screen.getByText(messages.totalLengthError)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should be disabled create button if form has error', async () => {
|
it('should be disabled create button if form has error', async () => {
|
||||||
render(<RootWrapper {...props} />);
|
render(<RootWrapper {...props} />);
|
||||||
await mockStore();
|
await mockStore();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
|||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
import { RequestStatus } from '../../data/constants';
|
import { RequestStatus, MAX_TOTAL_LENGTH, TOTAL_LENGTH_KEY } from '../../data/constants';
|
||||||
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
||||||
import {
|
import {
|
||||||
getRedirectUrlObj,
|
getRedirectUrlObj,
|
||||||
@@ -58,6 +58,12 @@ const useCreateOrRerunCourse = (initialValues) => {
|
|||||||
intl.formatMessage(messages.disallowedCharsError),
|
intl.formatMessage(messages.disallowedCharsError),
|
||||||
)
|
)
|
||||||
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
||||||
|
}).test(TOTAL_LENGTH_KEY, intl.formatMessage(messages.totalLengthError), function validateTotalLength() {
|
||||||
|
const { org, number, run } = this?.options.originalValue || {};
|
||||||
|
if ((org?.length || 0) + (number?.length || 0) + (run?.length || 0) > MAX_TOTAL_LENGTH) {
|
||||||
|
return this.createError({ path: TOTAL_LENGTH_KEY, message: intl.formatMessage(messages.totalLengthError) });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -76,7 +82,11 @@ const useCreateOrRerunCourse = (initialValues) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormFilled(Object.values(values).every((i) => i));
|
setFormFilled(
|
||||||
|
Object.entries(values)
|
||||||
|
?.filter(([key]) => key !== 'undefined')
|
||||||
|
.every(([, value]) => value),
|
||||||
|
);
|
||||||
dispatch(updatePostErrors({}));
|
dispatch(updatePostErrors({}));
|
||||||
}, [values]);
|
}, [values]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
import { MAX_TOTAL_LENGTH } from '../../data/constants';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
courseDisplayNameLabel: {
|
courseDisplayNameLabel: {
|
||||||
@@ -117,6 +118,10 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.create-or-rerun-course.no-space.error',
|
id: 'course-authoring.create-or-rerun-course.no-space.error',
|
||||||
defaultMessage: 'Please do not use any spaces in this field.',
|
defaultMessage: 'Please do not use any spaces in this field.',
|
||||||
},
|
},
|
||||||
|
totalLengthError: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.total-length-error.error',
|
||||||
|
defaultMessage: `The combined length of the organization, course number and course run fields cannot be more than ${MAX_TOTAL_LENGTH} characters.`,
|
||||||
|
},
|
||||||
alertErrorExistsAriaLabelledBy: {
|
alertErrorExistsAriaLabelledBy: {
|
||||||
id: 'course-authoring.create-or-rerun-course.error.already-exists.labelledBy',
|
id: 'course-authoring.create-or-rerun-course.error.already-exists.labelledBy',
|
||||||
defaultMessage: 'alert-already-exists-title',
|
defaultMessage: 'alert-already-exists-title',
|
||||||
|
|||||||
@@ -48,6 +48,26 @@ describe('<DeadlineSection />', () => {
|
|||||||
expect(testObj.gracePeriod.minutes).toBe(13);
|
expect(testObj.gracePeriod.minutes).toBe(13);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('checking deadline input value if grace Period has no hours', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper
|
||||||
|
gracePeriod={{ hours: 0, minutes: 13 }}
|
||||||
|
setGradingData={setGradingData}
|
||||||
|
/>);
|
||||||
|
await waitFor(() => {
|
||||||
|
const inputElement = getByTestId('deadline-period-input');
|
||||||
|
expect(inputElement.value).toBe('00:13');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('checking deadline input value if grace Period has no minutes', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper
|
||||||
|
gracePeriod={{ hours: 13, minutes: 0 }}
|
||||||
|
setGradingData={setGradingData}
|
||||||
|
/>);
|
||||||
|
await waitFor(() => {
|
||||||
|
const inputElement = getByTestId('deadline-period-input');
|
||||||
|
expect(inputElement.value).toBe('13:00');
|
||||||
|
});
|
||||||
|
});
|
||||||
it('checking deadline input value if grace Period equal null', async () => {
|
it('checking deadline input value if grace Period equal null', async () => {
|
||||||
const { getByTestId } = render(<RootWrapper gracePeriod={null} setGradingData={setGradingData} />);
|
const { getByTestId } = render(<RootWrapper gracePeriod={null} setGradingData={setGradingData} />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const DeadlineSection = ({
|
|||||||
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert,
|
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert,
|
||||||
}) => {
|
}) => {
|
||||||
const timeStampValue = gracePeriod
|
const timeStampValue = gracePeriod
|
||||||
? gracePeriod.hours && `${formatTime(gracePeriod.hours)}:${formatTime(gracePeriod.minutes)}`
|
? `${formatTime(gracePeriod.hours)}:${formatTime(gracePeriod.minutes)}`
|
||||||
: DEFAULT_TIME_STAMP;
|
: DEFAULT_TIME_STAMP;
|
||||||
const [newDeadlineValue, setNewDeadlineValue] = useState(timeStampValue);
|
const [newDeadlineValue, setNewDeadlineValue] = useState(timeStampValue);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|||||||
@@ -31,32 +31,37 @@ export const getContentMenuItems = ({ studioBaseUrl, courseId, intl }) => {
|
|||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSettingMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
export const getSettingMenuItems = ({ studioBaseUrl, courseId, intl }) => {
|
||||||
{
|
const items = [
|
||||||
href: `${studioBaseUrl}/settings/details/${courseId}`,
|
{
|
||||||
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
|
href: `${studioBaseUrl}/settings/details/${courseId}`,
|
||||||
},
|
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
|
||||||
{
|
},
|
||||||
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
{
|
||||||
title: intl.formatMessage(messages['header.links.grading']),
|
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
||||||
},
|
title: intl.formatMessage(messages['header.links.grading']),
|
||||||
{
|
},
|
||||||
href: `${studioBaseUrl}/course_team/${courseId}`,
|
{
|
||||||
title: intl.formatMessage(messages['header.links.courseTeam']),
|
href: `${studioBaseUrl}/course_team/${courseId}`,
|
||||||
},
|
title: intl.formatMessage(messages['header.links.courseTeam']),
|
||||||
{
|
},
|
||||||
href: `${studioBaseUrl}/group_configurations/${courseId}`,
|
{
|
||||||
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
href: `${studioBaseUrl}/group_configurations/${courseId}`,
|
||||||
},
|
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
||||||
{
|
},
|
||||||
href: `${studioBaseUrl}/settings/advanced/${courseId}`,
|
{
|
||||||
title: intl.formatMessage(messages['header.links.advancedSettings']),
|
href: `${studioBaseUrl}/settings/advanced/${courseId}`,
|
||||||
},
|
title: intl.formatMessage(messages['header.links.advancedSettings']),
|
||||||
{
|
},
|
||||||
href: `${studioBaseUrl}/certificates/${courseId}`,
|
];
|
||||||
title: intl.formatMessage(messages['header.links.certificates']),
|
if (getConfig().ENABLE_CERTIFICATE_PAGE === 'true') {
|
||||||
},
|
items.push({
|
||||||
]);
|
href: `${studioBaseUrl}/certificates/${courseId}`,
|
||||||
|
title: intl.formatMessage(messages['header.links.certificates']),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
export const getToolsMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
export const getToolsMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
||||||
{
|
{
|
||||||
@@ -69,7 +74,7 @@ export const getToolsMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
|||||||
},
|
},
|
||||||
...(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'
|
...(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'
|
||||||
? [{
|
? [{
|
||||||
href: `${studioBaseUrl}/api/content_tagging/v1/object_tags/${courseId}/export/`,
|
href: '#export-tags',
|
||||||
title: intl.formatMessage(messages['header.links.exportTags']),
|
title: intl.formatMessage(messages['header.links.exportTags']),
|
||||||
}] : []
|
}] : []
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||||
import { getContentMenuItems, getToolsMenuItems } from './utils';
|
import { getContentMenuItems, getToolsMenuItems, getSettingMenuItems } from './utils';
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
studioBaseUrl: 'UrLSTuiO',
|
studioBaseUrl: 'UrLSTuiO',
|
||||||
@@ -29,6 +29,25 @@ describe('header utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getSettingsMenuitems', () => {
|
||||||
|
it('should include certificates option', () => {
|
||||||
|
setConfig({
|
||||||
|
...getConfig(),
|
||||||
|
ENABLE_CERTIFICATE_PAGE: 'true',
|
||||||
|
});
|
||||||
|
const actualItems = getSettingMenuItems(props);
|
||||||
|
expect(actualItems).toHaveLength(6);
|
||||||
|
});
|
||||||
|
it('should not include certificates option', () => {
|
||||||
|
setConfig({
|
||||||
|
...getConfig(),
|
||||||
|
ENABLE_CERTIFICATE_PAGE: 'false',
|
||||||
|
});
|
||||||
|
const actualItems = getSettingMenuItems(props);
|
||||||
|
expect(actualItems).toHaveLength(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getToolsMenuItems', () => {
|
describe('getToolsMenuItems', () => {
|
||||||
it('should include export tags option', () => {
|
it('should include export tags option', () => {
|
||||||
setConfig({
|
setConfig({
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ initialize({
|
|||||||
ENABLE_UNIT_PAGE: process.env.ENABLE_UNIT_PAGE || 'false',
|
ENABLE_UNIT_PAGE: process.env.ENABLE_UNIT_PAGE || 'false',
|
||||||
ENABLE_ASSETS_PAGE: process.env.ENABLE_ASSETS_PAGE || 'false',
|
ENABLE_ASSETS_PAGE: process.env.ENABLE_ASSETS_PAGE || 'false',
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN || 'false',
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN || 'false',
|
||||||
|
ENABLE_CERTIFICATE_PAGE: process.env.ENABLE_CERTIFICATE_PAGE || 'false',
|
||||||
ENABLE_TAGGING_TAXONOMY_PAGES: process.env.ENABLE_TAGGING_TAXONOMY_PAGES || 'false',
|
ENABLE_TAGGING_TAXONOMY_PAGES: process.env.ENABLE_TAGGING_TAXONOMY_PAGES || 'false',
|
||||||
ENABLE_HOME_PAGE_COURSE_API_V2: process.env.ENABLE_HOME_PAGE_COURSE_API_V2 === 'true',
|
ENABLE_HOME_PAGE_COURSE_API_V2: process.env.ENABLE_HOME_PAGE_COURSE_API_V2 === 'true',
|
||||||
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
|
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const AppSettingsModalBase = ({
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
hasCloseButton={isMobile}
|
hasCloseButton={isMobile}
|
||||||
isFullscreenOnMobile
|
isFullscreenOnMobile
|
||||||
|
isOverflowVisible={false}
|
||||||
>
|
>
|
||||||
<ModalDialog.Header>
|
<ModalDialog.Header>
|
||||||
<ModalDialog.Title data-testid="modal-title">{title}</ModalDialog.Title>
|
<ModalDialog.Title data-testid="modal-title">{title}</ModalDialog.Title>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function getItemIcon(blockType) {
|
|||||||
*/
|
*/
|
||||||
function getLibraryHitUrl(hit, libraryAuthoringMfeUrl) {
|
function getLibraryHitUrl(hit, libraryAuthoringMfeUrl) {
|
||||||
const { contextKey } = hit;
|
const { contextKey } = hit;
|
||||||
return `${libraryAuthoringMfeUrl}/library/${contextKey}`;
|
return `${libraryAuthoringMfeUrl}library/${contextKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,10 +62,20 @@ function getUnitUrlSuffix(hit) {
|
|||||||
function getUnitComponentUrlSuffix(hit) {
|
function getUnitComponentUrlSuffix(hit) {
|
||||||
const { breadcrumbs, contextKey, usageKey } = hit;
|
const { breadcrumbs, contextKey, usageKey } = hit;
|
||||||
if (breadcrumbs.length > 1) {
|
if (breadcrumbs.length > 1) {
|
||||||
const parent = breadcrumbs[breadcrumbs.length - 1];
|
let parent = breadcrumbs[breadcrumbs.length - 1];
|
||||||
|
|
||||||
if ('usageKey' in parent) {
|
if ('usageKey' in parent) {
|
||||||
return `course/${contextKey}/container/${parent.usageKey}?show=${encodeURIComponent(usageKey)}`;
|
// Handle case for library component in unit
|
||||||
|
let libComponentUsageKey;
|
||||||
|
if (parent.usageKey.includes('type@library_content') && breadcrumbs.length > 2) {
|
||||||
|
libComponentUsageKey = parent.usageKey;
|
||||||
|
parent = breadcrumbs[breadcrumbs.length - 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('usageKey' in parent) {
|
||||||
|
const encodedUsageKey = encodeURIComponent(libComponentUsageKey || usageKey);
|
||||||
|
return `course/${contextKey}/container/${parent.usageKey}?show=${encodedUsageKey}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +106,13 @@ function getUrlSuffix(hit) {
|
|||||||
return getUnitUrlSuffix(hit);
|
return getUnitUrlSuffix(hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the parent is a unit
|
// Check if the parent is a unit or a library component in a unit
|
||||||
if (breadcrumbs.length > 1) {
|
if (breadcrumbs.length > 1) {
|
||||||
const parent = breadcrumbs[breadcrumbs.length - 1];
|
const parent = breadcrumbs[breadcrumbs.length - 1];
|
||||||
|
|
||||||
if ('usageKey' in parent && parent.usageKey.includes('type@vertical')) {
|
if ('usageKey' in parent && (
|
||||||
|
parent.usageKey.includes('type@vertical') || parent.usageKey.includes('type@library_content'))
|
||||||
|
) {
|
||||||
return getUnitComponentUrlSuffix(hit);
|
return getUnitComponentUrlSuffix(hit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,6 +290,31 @@ describe('<SearchUI />', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('click lib component in unit result navigates to the context of encompassing lib component', async () => {
|
||||||
|
const { findAllByRole } = rendered;
|
||||||
|
|
||||||
|
const [resultItem] = await findAllByRole('button', { name: /Text block in Lib Component/ });
|
||||||
|
|
||||||
|
// Clicking the "Open in new window" button should open the result in a new window:
|
||||||
|
const { open } = window;
|
||||||
|
window.open = jest.fn();
|
||||||
|
fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
|
||||||
|
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
|
||||||
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40library_content%2Bblock%40427e5cd03fbe431d9d551c67d4e280ae',
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
window.open = open;
|
||||||
|
|
||||||
|
// Clicking in the result should navigate to the result's URL:
|
||||||
|
fireEvent.click(resultItem);
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith(
|
||||||
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
|
||||||
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40library_content%2Bblock%40427e5cd03fbe431d9d551c67d4e280ae',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('click lib component result navigates to the context', async () => {
|
test('click lib component result navigates to the context', async () => {
|
||||||
const data = generateGetStudioHomeDataApiResponse();
|
const data = generateGetStudioHomeDataApiResponse();
|
||||||
data.redirectToLibraryAuthoringMfe = true;
|
data.redirectToLibraryAuthoringMfe = true;
|
||||||
|
|||||||
@@ -262,6 +262,76 @@
|
|||||||
"org": "SampleTaxonomyOrg1",
|
"org": "SampleTaxonomyOrg1",
|
||||||
"access_id": "6"
|
"access_id": "6"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Text block in Lib Component",
|
||||||
|
"block_id": "b654d61248bcc1f84c08",
|
||||||
|
"content": {
|
||||||
|
"html_content": " This is a text block lib component. "
|
||||||
|
},
|
||||||
|
"id": "block-v1sampletaxonomyorg1stc12023_1typehtmlblockb654d61248bcc1f84c08-77f1f658",
|
||||||
|
"type": "course_block",
|
||||||
|
"breadcrumbs": [
|
||||||
|
{
|
||||||
|
"display_name": "Sample Taxonomy Course"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Section 1",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Subsection 1.1",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Unit 1.1.1",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Randomized Content Block",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@library_content+block@427e5cd03fbe431d9d551c67d4e280ae"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@html+block@b654d61248bcc1f84c08",
|
||||||
|
"block_type": "html",
|
||||||
|
"context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
|
||||||
|
"org": "SampleTaxonomyOrg1",
|
||||||
|
"access_id": 6,
|
||||||
|
"_formatted": {
|
||||||
|
"display_name": "Text block in Lib Component",
|
||||||
|
"block_id": "b654d61248bcc1f84c08",
|
||||||
|
"content": {
|
||||||
|
"html_content": " This is a text block lib component. "
|
||||||
|
},
|
||||||
|
"id": "block-v1sampletaxonomyorg1stc12023_1typehtmlblockb654d61248bcc1f84c08-77f1f658",
|
||||||
|
"type": "course_block",
|
||||||
|
"breadcrumbs": [
|
||||||
|
{
|
||||||
|
"display_name": "Sample Taxonomy Course"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Section 1",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Subsection 1.1",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Unit 1.1.1",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"display_name": "Randomized Content Block",
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@library_content+block@427e5cd03fbe431d9d551c67d4e280ae"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@html+block@b654d61248bcc1f84c08",
|
||||||
|
"block_type": "html",
|
||||||
|
"context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
|
||||||
|
"org": "SampleTaxonomyOrg1",
|
||||||
|
"access_id": "6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"query": "learn",
|
"query": "learn",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const StudioHome = ({ intl }) => {
|
|||||||
studioRequestEmail,
|
studioRequestEmail,
|
||||||
libraryAuthoringMfeUrl,
|
libraryAuthoringMfeUrl,
|
||||||
redirectToLibraryAuthoringMfe,
|
redirectToLibraryAuthoringMfe,
|
||||||
|
showNewLibraryButton,
|
||||||
} = studioHomeData;
|
} = studioHomeData;
|
||||||
|
|
||||||
function getHeaderButtons() {
|
function getHeaderButtons() {
|
||||||
@@ -78,23 +79,25 @@ const StudioHome = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`;
|
if (showNewLibraryButton) {
|
||||||
if (redirectToLibraryAuthoringMfe) {
|
let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`;
|
||||||
libraryHref = `${libraryAuthoringMfeUrl}/create`;
|
if (redirectToLibraryAuthoringMfe) {
|
||||||
}
|
libraryHref = `${libraryAuthoringMfeUrl}/create`;
|
||||||
|
}
|
||||||
|
|
||||||
headerButtons.push(
|
headerButtons.push(
|
||||||
<Button
|
<Button
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
iconBefore={AddIcon}
|
iconBefore={AddIcon}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={showNewCourseContainer}
|
disabled={showNewCourseContainer}
|
||||||
href={libraryHref}
|
href={libraryHref}
|
||||||
data-testid="new-library-button"
|
data-testid="new-library-button"
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.addNewLibraryBtnText)}
|
{intl.formatMessage(messages.addNewLibraryBtnText)}
|
||||||
</Button>,
|
</Button>,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return headerButtons;
|
return headerButtons;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,15 @@ describe('<StudioHome />', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('do not render new library button if showNewLibraryButton is False', () => {
|
||||||
|
useSelector.mockReturnValue({
|
||||||
|
...studioHomeMock,
|
||||||
|
showNewLibraryButton: false,
|
||||||
|
});
|
||||||
|
const { queryByTestId } = render(<RootWrapper />);
|
||||||
|
expect(queryByTestId('new-library-button')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should render create new course container', async () => {
|
it('should render create new course container', async () => {
|
||||||
useSelector.mockReturnValue({
|
useSelector.mockReturnValue({
|
||||||
...studioHomeMock,
|
...studioHomeMock,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const generateGetStudioHomeDataApiResponse = () => ({
|
|||||||
inProcessCourseActions: [],
|
inProcessCourseActions: [],
|
||||||
libraries: [],
|
libraries: [],
|
||||||
librariesEnabled: true,
|
librariesEnabled: true,
|
||||||
libraryAuthoringMfeUrl: 'http://localhost:3001',
|
libraryAuthoringMfeUrl: 'http://localhost:3001/',
|
||||||
optimizationEnabled: false,
|
optimizationEnabled: false,
|
||||||
redirectToLibraryAuthoringMfe: false,
|
redirectToLibraryAuthoringMfe: false,
|
||||||
requestCourseCreatorUrl: '/request_course_creator',
|
requestCourseCreatorUrl: '/request_course_creator',
|
||||||
|
|||||||
Reference in New Issue
Block a user