Compare commits

..

1 Commits

Author SHA1 Message Date
Alie Langston
8a5687bfe3 feat: gate learning assistant if learner is in exam 2023-11-13 14:36:49 -06:00
277 changed files with 12748 additions and 13178 deletions

View File

@@ -3,5 +3,3 @@ dist/
packages/
node_modules/
jest.config.js
env.config.jsx
example.env.config.jsx

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
const config = createConfig('eslint', {
rules: {
@@ -12,13 +12,6 @@ const config = createConfig('eslint', {
'react/no-unknown-property': 'off',
'func-names': 'off',
},
settings: {
'import/resolver': {
webpack: {
config: 'webpack.prod.config.js',
},
},
},
});
module.exports = config;

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

View File

@@ -18,7 +18,6 @@ jobs:
node-version: ${{ env.NODE_VER }}
- run: make validate.ci
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

5
.gitignore vendored
View File

@@ -6,7 +6,6 @@
node_modules
npm-debug.log
coverage
env.config.*
dist/
src/i18n/transifex_input.json
@@ -26,7 +25,3 @@ module.config.js
# Local environment overrides
.env.private
src/i18n/messages/
env.config.jsx

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-learning]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -1,17 +1,20 @@
export TRANSIFEX_RESOURCE=frontend-app-learning
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fa_IR,fr_CA,it_IT,pt_PT,de_DE"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs
transifex_temp = ./temp/babel-plugin-react-intl
precommit:
npm run lint
npm audit
requirements:
npm ci
npm install
i18n.extract:
# Pulling display strings from .jsx files into .json files...
@@ -29,21 +32,35 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning
&& atlas pull --filter=$(transifex_langs) \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-learning
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,6 +1,6 @@
#####################
frontend-app-learning
#####################
######################
|codecov| |license|
@@ -84,10 +84,10 @@ file (which is git-ignored) that defines where to find your local modules, for i
may want to use "src" if the module installs React as a peer/dev dependency.
*/
localModules: [
{ moduleName: '@openedx/paragon/scss', dir: '../paragon', dist: 'scss' },
{ moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
{ moduleName: '@openedx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
{ moduleName: '@openedx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
{ moduleName: '@edx/paragon/scss', dir: '../paragon', dist: 'scss' },
{ moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
{ moduleName: '@edx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
{ moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
],
};
@@ -100,12 +100,6 @@ The Learning MFE is similar to all the other Open edX MFEs. Read the Open
edX Developer Guide's section on
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Environment Variables
======================

View File

@@ -1,18 +0,0 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-learning'
description: "This is the Learning MFE, which renders all learner-facing course pages."
links:
- url: "https://github.com/openedx/frontend-app-learning"
title: "Learning MFE"
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:2u-aurora
type: 'website'
lifecycle: 'production'

View File

@@ -1,24 +0,0 @@
import UnitTranslationPlugin from '@edx/unit-translation-selector-plugin';
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
// Load environment variables from .env file
const config = {
...process.env,
pluginSlots: {
unit_title_plugin: {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'unit_title_plugin',
type: DIRECT_PLUGIN,
priority: 1,
RenderWidget: UnitTranslationPlugin,
},
},
],
},
},
};
export default config;

View File

@@ -1,4 +0,0 @@
// Force all tests to run in UTC to prevent tests from being sensitive to host timezone.
module.exports = async () => {
process.env.TZ = 'UTC';
};

View File

@@ -1,6 +1,6 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
const config = createConfig('jest', {
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js',
],
@@ -14,30 +14,7 @@ const config = createConfig('jest', {
"^axios$": "axios/dist/axios.js",
// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
'@src/(.*)': '<rootDir>/src/$1',
},
testTimeout: 30000,
globalSetup: "./global-setup.js",
verbose: true,
testEnvironment: 'jsdom',
testEnvironment: 'jsdom'
});
// delete config.testURL;
config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
// change this setting if need to see less details for each test
// reportType: "summary" | "details",
// enable: true | false,
afterEachTest: {
enable: true,
filePaths: false,
reportType: "details",
},
afterAllTests: {
reportType: "summary",
enable: true,
filePaths: true,
},
}]];
module.exports = config;

10880
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,11 +11,10 @@
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"prepare": "husky install",
"postinstall": "patch-package",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
@@ -31,20 +30,18 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.0.2",
"@edx/frontend-lib-learning-assistant": "^2.0.0",
"@edx/frontend-lib-special-exams": "^3.0.1",
"@edx/frontend-platform": "^7.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^2.0.0",
"@edx/frontend-component-footer": "12.2.1",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-lib-learning-assistant": "^1.17.0",
"@edx/frontend-lib-special-exams": "2.23.3",
"@edx/frontend-platform": "5.5.2",
"@edx/paragon": "20.46.0",
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.7.0",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "^0.1.4",
"@openedx/frontend-plugin-framework": "^1.1.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.3.0",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.8.1",
"classnames": "2.3.2",
@@ -70,8 +67,8 @@
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-build": "^12.9.10",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "13.1.4",
"@pact-foundation/pact": "^11.0.2",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
@@ -80,17 +77,8 @@
"axios-mock-adapter": "1.20.0",
"copy-webpack-plugin": "^11.0.0",
"es-check": "6.2.1",
"eslint-import-resolver-webpack": "^0.13.8",
"husky": "7.0.4",
"jest": "^26.6.3",
"jest-console-group-reporter": "^1.0.1",
"jest-when": "^3.6.0",
"patch-package": "^8.0.0",
"postcss-loader": "^8.1.1",
"rosie": "2.1.1",
"sass": "^1.72.0",
"sass-loader": "^14.1.1",
"source-map-loader": "^5.0.0",
"style-loader": "^3.3.4"
"jest": "29.5.0",
"rosie": "2.1.0"
}
}

View File

@@ -1,36 +0,0 @@
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
index 2879dd9..9efd0fc 100644
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
+const fs = require('fs');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const PostCssRTLCSS = require('postcss-rtlcss');
const PostCssCustomMediaCSS = require('postcss-custom-media');
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
const commonConfig = require('./webpack.common.config');
const presets = require('../lib/presets');
+/**
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
+ * root directory. Its env variables can be accessed with getConfig().
+ *
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
+ */
+
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
+
+if (envConfigPath) {
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
+ fs.copyFileSync(envConfigPath, envConfigFilename);
+}
+
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
dotenv.config({
path: path.resolve(process.cwd(), '.env'),

View File

@@ -8,7 +8,7 @@
"rebaseStalePrs": true,
"packageRules": [
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchPackagePatterns": ["@edx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}

View File

@@ -3,8 +3,8 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
FormattedMessage, FormattedDate, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import messages from './messages';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { PageBanner } from '@openedx/paragon';
import { PageBanner } from '@edx/paragon';
const AccessExpirationMasqueradeBanner = ({ payload }) => {
const {

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert, Hyperlink } from '@openedx/paragon';
import { WarningFilled } from '@openedx/paragon/icons';
import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@edx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import genericMessages from './messages';

View File

@@ -6,8 +6,8 @@ import {
FormattedRelativeTime,
FormattedTime,
} from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { useModel } from '../../generic/model-store';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { PageBanner } from '@openedx/paragon';
import { PageBanner } from '@edx/paragon';
import { useModel } from '../../generic/model-store';

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert, Button } from '@openedx/paragon';
import { Info, WarningFilled } from '@openedx/paragon/icons';
import { Alert, Button } from '@edx/paragon';
import { Info, WarningFilled } from '@edx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';

View File

@@ -7,8 +7,8 @@ import {
Button,
Spinner,
Icon,
} from '@openedx/paragon';
import { Check, ArrowForward } from '@openedx/paragon/icons';
} from '@edx/paragon';
import { Check, ArrowForward } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendActivationEmail } from '../../courseware/data';
import messages from './messages';

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Alert, Hyperlink } from '@openedx/paragon';
import { WarningFilled } from '@openedx/paragon/icons';
import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@edx/paragon/icons';
import genericMessages from '../../generic/messages';

View File

@@ -22,30 +22,12 @@ export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token',
REDIRECT: '/redirect/*',
DASHBOARD: 'dashboard',
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
CONSENT: 'consent',
};
export const REDIRECT_MODES = {
DASHBOARD_REDIRECT: 'dashboard-redirect',
ENTERPRISE_LEARNER_DASHBOARD_REDIRECT: 'enterprise-learner-dashboard-redirect',
CONSENT_REDIRECT: 'consent-redirect',
HOME_REDIRECT: 'home-redirect',
SURVEY_REDIRECT: 'survey-redirect',
};
export const VERIFIED_MODES = [
'professional',
'verified',
'no-id-professional',
'credit',
'masters',
'executive-education',
'paid-executive-education',
'paid-bootcamp',
];
export const WIDGETS = {
DISCUSSIONS: 'DISCUSSIONS',
NOTIFICATIONS: 'NOTIFICATIONS',
};

View File

@@ -1,72 +1,54 @@
import React, { useMemo } from 'react';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Tabs, Tab } from '@openedx/paragon';
import { Tabs, Tab } from '@edx/paragon';
import { useParams } from 'react-router';
import CoursewareSearchResults from './CoursewareSearchResults';
import messages from './messages';
import { useCoursewareSearchParams } from './hooks';
import { useModel } from '../../generic/model-store';
const filterAll = 'all';
const filterTypes = ['text', 'video', 'sequence'];
const filterOther = 'other';
const validFilters = [filterAll, ...filterTypes, filterOther];
const noFilterKey = 'none';
const noFilterLabel = 'All content';
export const filteredResultsBySelection = ({ key = noFilterKey, results = [] }) => (
key === noFilterKey ? results : results.filter(({ type }) => type === key)
);
export const CoursewareSearchResultsFilter = ({ intl }) => {
const { courseId } = useParams();
const lastSearch = useModel('contentSearchResults', courseId);
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
if (!lastSearch) { return null; }
if (!lastSearch || !lastSearch?.results?.length) { return null; }
const { results: data = [] } = lastSearch;
const { total, results } = lastSearch;
// If there's no data, we show an empty result.
if (!data.length) { return <CoursewareSearchResults />; }
const filters = [
{
key: noFilterKey,
label: noFilterLabel,
count: total,
},
...lastSearch.filters,
];
const results = useMemo(() => {
// This reducer distributes the data into different groups to make it easy to
// use on the filters.
// All results are added to the "all" key and then to its proper group key as well.
const grouped = data.reduce((acc, { type, ...rest }) => {
const resultType = filterTypes.includes(type) ? type : filterOther;
acc[filterAll].push({ type: resultType, ...rest });
acc[resultType] = [...(acc[resultType] || []), { type: resultType, ...rest }];
return acc;
}, { [filterAll]: [] });
// This is just to format the output object with the expected tab order.
const output = {};
validFilters.forEach(key => { if (grouped[key]) { output[key] = grouped[key]; } });
return output;
}, [lastSearch]);
const tabKeys = Object.keys(results);
// Filter has no use if it has only 2 tabs (The "all" tab and another one with the same items).
if (tabKeys.length < 3) { return <CoursewareSearchResults results={results[filterAll]} />; }
const filters = useMemo(() => tabKeys.map((key) => ({
key,
label: intl.formatMessage(messages[`filter:${key}`]),
count: results[key].length,
})), [results]);
const activeKey = validFilters.includes(filterKeyword) ? filterKeyword : filterAll;
const getFilterTitle = (key, fallback) => {
const msg = messages[`filter:${key}`];
if (!msg) { return fallback; }
return intl.formatMessage(msg);
};
return (
<Tabs
id="courseware-search-results-tabs"
className="courseware-search-results-tabs"
data-testid="courseware-search-results-tabs"
variant="tabs"
activeKey={activeKey}
onSelect={setFilter}
defaultActiveKey={noFilterKey}
>
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}>
<CoursewareSearchResults results={results[key]} />
{filters.map(({ key, label }) => (
<Tab key={key} eventKey={key} title={getFilterTitle(key, label)}>
<CoursewareSearchResults
results={filteredResultsBySelection({ key, results })}
/>
</Tab>
))}
</Tabs>

View File

@@ -8,22 +8,34 @@ import {
screen,
waitFor,
} from '../../setupTest';
import { CoursewareSearchResultsFilter } from './CoursewareResultsFilter';
import { useCoursewareSearchParams } from './hooks';
import { CoursewareSearchResultsFilter, filteredResultsBySelection } from './CoursewareResultsFilter';
import initializeStore from '../../store';
import { useModel } from '../../generic/model-store';
import searchResultsFactory from './test-data/search-results-factory';
jest.mock('./hooks');
jest.mock('../../generic/model-store', () => ({
useModel: jest.fn(),
}));
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
const mockResults = [
{
id: 'video-1', type: 'video', title: 'video_title', score: 3, contentHits: 1, url: '/video-1', location: ['path1', 'path2'],
},
{
id: 'video-2', type: 'video', title: 'video_title2', score: 2, contentHits: 1, url: '/video-2', location: ['path1', 'path2'],
},
{
id: 'document-1', type: 'document', title: 'document_title', score: 3, contentHits: 1, url: '/document-1', location: ['path1', 'path2'],
},
{
id: 'text-1', type: 'text', title: 'text_title1', score: 3, contentHits: 1, url: '/text-1', location: ['path1', 'path2'],
},
{
id: 'text-2', type: 'text', title: 'text_title2', score: 2, contentHits: 1, url: '/text-2', location: ['path1', 'path2'],
},
{
id: 'text-3', type: 'text', title: 'text_title3', score: 1, contentHits: 1, url: '/text-3', location: ['path1', 'path2'],
},
];
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
@@ -34,14 +46,6 @@ const intl = {
formatMessage: (message) => message?.defaultMessage || '',
};
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
function renderComponent(props = {}) {
const store = initializeStore();
history.push(pathname);
@@ -58,75 +62,56 @@ function renderComponent(props = {}) {
describe('CoursewareSearchResultsFilter', () => {
beforeAll(initializeMockApp);
beforeEach(() => {
useCoursewareSearchParams.mockReturnValue(coursewareSearch);
describe('filteredResultsBySelection', () => {
it('returns a no values array when no results are provided', () => {
const results = filteredResultsBySelection({ results: [] });
expect(results.length).toEqual(0);
});
it('returns all values when no key value is provided', () => {
const results = filteredResultsBySelection({ results: mockResults });
expect(results.length).toEqual(mockResults.length);
});
it('returns only "video"-typed elements when the key value "video" is given', () => {
const results = filteredResultsBySelection({ key: 'video', results: mockResults });
expect(results.length).toEqual(2);
});
it('returns only "course_outline"-typed elements when the key value "document" is given', () => {
const results = filteredResultsBySelection({ key: 'document', results: mockResults });
expect(results.length).toEqual(1);
});
it('returns only "text"-typed elements when the key value "text" is given', () => {
const results = filteredResultsBySelection({ key: 'text', results: mockResults });
expect(results.length).toEqual(3);
});
});
afterEach(() => {
jest.clearAllMocks();
});
describe('when returning full results', () => {
describe('</CoursewareSearchResultsFilter />', () => {
beforeEach(() => {
useModel.mockReturnValue(searchResultsFactory());
renderComponent();
jest.clearAllMocks();
});
it('should render without errors', async () => {
await waitFor(() => {
expect(useCoursewareSearchParams).toBeCalled();
});
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
});
});
describe('when returning only one result type', () => {
beforeEach(async () => {
// Get results for only videos
const data = searchResultsFactory();
const onlyVideos = data.results.filter(({ type }) => type === 'video');
const filteredResults = {
...data,
results: onlyVideos,
};
useModel.mockReturnValue(filteredResults);
await renderComponent();
});
it('should not render', async () => {
await waitFor(() => {
expect(useCoursewareSearchParams).toBeCalled();
});
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
});
});
describe('when there are not results', () => {
beforeEach(async () => {
useModel.mockReturnValue(searchResultsFactory('blah', {
results: [],
it('should render', async () => {
useModel.mockReturnValue({
total: 6,
results: mockResults,
filters: [],
total: 0,
maxScore: null,
ms: 5,
}));
await renderComponent();
});
it('should not render', async () => {
await waitFor(() => {
expect(useCoursewareSearchParams).toBeCalled();
});
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
await renderComponent();
await waitFor(() => {
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
expect(screen.queryByText(/All content/)).toBeInTheDocument();
});
});
});
});

View File

@@ -1,16 +1,16 @@
import React, { useEffect } from 'react';
import React, { useState } from 'react';
import { useParams } from 'react-router';
import { useDispatch } from 'react-redux';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Alert, Button, Icon, Spinner,
} from '@openedx/paragon';
} from '@edx/paragon';
import {
Close,
} from '@openedx/paragon/icons';
} from '@edx/paragon/icons';
import { setShowSearch } from '../data/slice';
import { useCoursewareSearchParams, useElementBoundingBox, useLockScroll } from './hooks';
import { useElementBoundingBox, useLockScroll } from './hooks';
import messages from './messages';
import CoursewareSearchForm from './CoursewareSearchForm';
@@ -20,7 +20,6 @@ import { searchCourseContent } from '../data/thunks';
const CoursewareSearch = ({ intl, ...sectionProps }) => {
const { courseId } = useParams();
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
const dispatch = useDispatch();
const { org } = useModel('courseHomeMeta', courseId);
const {
@@ -29,6 +28,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
errors,
total,
} = useModel('contentSearchResults', courseId);
const [searchKeyword, setSearchKeyword] = useState(lastSearchKeyword);
useLockScroll();
@@ -36,7 +36,6 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
const top = info ? `${Math.floor(info.top)}px` : 0;
const clearSearch = () => {
clearSearchParams();
dispatch(updateModel({
modelType: 'contentSearchResults',
model: {
@@ -49,35 +48,34 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
}));
};
const handleSubmit = (value) => {
if (!value) {
const handleSubmit = () => {
if (!searchKeyword) {
clearSearch();
return;
}
sendTrackingLogEvent('edx.course.home.courseware_search.submit', {
const eventProperties = {
org_key: org,
courserun_key: courseId,
};
sendTrackingLogEvent('edx.course.home.courseware_search.submit', {
...eventProperties,
event_type: 'searchKeyword',
keyword: value,
keyword: searchKeyword,
});
dispatch(searchCourseContent(courseId, value));
setQuery(value);
dispatch(searchCourseContent(courseId, searchKeyword));
};
useEffect(() => {
handleSubmit(searchKeyword);
}, []);
const handleOnChange = (value) => {
if (value === searchKeyword) { return; }
if (!value) { clearSearch(); }
};
const handleSearchCloseClick = () => {
clearSearch();
dispatch(setShowSearch(false));
setSearchKeyword(value);
if (!value) {
clearSearch();
}
};
let status = 'idle';
@@ -96,14 +94,14 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
variant="tertiary"
className="p-1"
aria-label={intl.formatMessage(messages.searchCloseAction)}
onClick={handleSearchCloseClick}
onClick={() => dispatch(setShowSearch(false))}
data-testid="courseware-search-close-button"
><Icon src={Close} />
</Button>
</div>
<div className="courseware-search__outer-content">
<div className="courseware-search__content">
<h1 className="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1>
<h2>{intl.formatMessage(messages.searchModuleTitle)}</h2>
<CoursewareSearchForm
searchTerm={searchKeyword}
onSubmit={handleSubmit}
@@ -111,27 +109,27 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
placeholder={intl.formatMessage(messages.searchBarPlaceholderText)}
/>
{status === 'loading' ? (
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
<div className="courseware-search__spinner">
<Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} />
</div>
) : null}
{status === 'error' && (
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
<Alert className="mt-4" variant="danger">
{intl.formatMessage(messages.searchResultsError)}
</Alert>
)}
{status === 'results' ? (
<>
{total > 0 ? (
<div
className="courseware-search__results-summary"
aria-live="polite"
aria-relevant="all"
aria-atomic="true"
data-testid="courseware-search-summary"
>{intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
</div>
) : null}
<div className="courseware-search__results-summary">{total > 0
? (
intl.formatMessage(
total === 1
? messages.searchResultsSingular
: messages.searchResultsPlural,
{ total, keyword: lastSearchKeyword },
)
) : intl.formatMessage(messages.searchResultsNone)}
</div>
<CoursewareSearchResultsFilterContainer />
</>
) : null}

View File

@@ -2,45 +2,21 @@ import React from 'react';
import { history } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { Route, Routes } from 'react-router-dom';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import {
initializeMockApp,
render,
screen,
waitFor,
fireEvent,
} from '../../setupTest';
import { CoursewareSearch } from './index';
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks';
import { useElementBoundingBox, useLockScroll } from './hooks';
import initializeStore from '../../store';
import { searchCourseContent } from '../data/thunks';
import { setShowSearch } from '../data/slice';
import { updateModel, useModel } from '../../generic/model-store';
import { useModel } from '../../generic/model-store';
jest.mock('./hooks');
jest.mock('../../generic/model-store', () => ({
updateModel: jest.fn(),
useModel: jest.fn(),
}));
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackingLogEvent: jest.fn(),
}));
jest.mock('../data/thunks', () => ({
searchCourseContent: jest.fn(),
}));
jest.mock('../data/slice', () => ({
setShowSearch: jest.fn(),
}));
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc';
@@ -56,14 +32,6 @@ const defaultProps = {
total: 0,
};
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const intl = {
formatMessage: (message) => message?.defaultMessage || '',
};
@@ -82,22 +50,7 @@ function renderComponent(props = {}) {
}
const mockModels = ((props = defaultProps) => {
useModel.mockReturnValue({
...defaultProps,
...props,
});
updateModel.mockReturnValue({
type: 'MOCK_ACTION',
payload: {
modelType: 'contentSearchResults',
model: defaultProps,
},
});
});
const mockSearchParams = ((props = coursewareSearch) => {
useCoursewareSearchParams.mockReturnValue(props);
useModel.mockReturnValue(props);
});
describe('CoursewareSearch', () => {
@@ -112,18 +65,16 @@ describe('CoursewareSearch', () => {
useElementBoundingBox.mockImplementation(() => ({ top: tabsTopPosition }));
});
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
it('Should use useElementBoundingBox() and useLockScroll() hooks', () => {
mockModels();
mockSearchParams();
renderComponent();
expect(useElementBoundingBox).toBeCalledTimes(1);
expect(useLockScroll).toBeCalledTimes(1);
});
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
it('Should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
mockModels();
mockSearchParams();
renderComponent();
const section = screen.getByTestId('courseware-search-section');
@@ -131,26 +82,11 @@ describe('CoursewareSearch', () => {
});
});
describe('when clicking on the "Close" button', () => {
it('should dispatch setShowSearch(false)', async () => {
mockModels();
renderComponent();
await waitFor(() => {
const close = screen.queryByTestId('courseware-search-close-button');
fireEvent.click(close);
});
expect(setShowSearch).toBeCalledWith(false);
});
});
describe('when CourseTabsNavigation is not present', () => {
it('should use "--modal-top-position: 0" if nce element is not present', () => {
it('Should use "--modal-top-position: 0" if nce element is not present', () => {
useElementBoundingBox.mockImplementation(() => undefined);
mockModels();
mockSearchParams();
renderComponent();
const section = screen.getByTestId('courseware-search-section');
@@ -159,127 +95,12 @@ describe('CoursewareSearch', () => {
});
describe('when passing extra props', () => {
it('should pass on extra props to section element', () => {
it('Should pass on extra props to section element', () => {
mockModels();
mockSearchParams();
renderComponent({ foo: 'bar' });
const section = screen.getByTestId('courseware-search-section');
expect(section).toHaveAttribute('foo', 'bar');
});
});
describe('when submitting an empty search', () => {
it('should clear the search by dispatch updateModel', async () => {
mockModels();
renderComponent();
await waitFor(() => {
const submit = screen.queryByTestId('courseware-search-form-submit');
fireEvent.click(submit);
});
expect(updateModel).toHaveBeenCalledWith({
modelType: 'contentSearchResults',
model: {
id: decodedCourseId,
searchKeyword: '',
results: [],
errors: undefined,
loading: false,
},
});
});
});
describe('when submitting a search', () => {
it('should show a loading state', () => {
mockModels({
loading: true,
});
renderComponent();
expect(screen.queryByTestId('courseware-search-spinner')).toBeInTheDocument();
});
it('should call searchCourseContent', async () => {
mockModels();
renderComponent();
const searchKeyword = 'course';
await waitFor(() => {
const input = screen.queryByTestId('courseware-search-form').querySelector('input');
fireEvent.change(input, { target: { value: searchKeyword } });
});
await waitFor(() => {
const submit = screen.queryByTestId('courseware-search-form-submit');
fireEvent.click(submit);
});
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.course.home.courseware_search.submit', {
org_key: defaultProps.org,
courserun_key: decodedCourseId,
event_type: 'searchKeyword',
keyword: searchKeyword,
});
expect(searchCourseContent).toHaveBeenCalledWith(decodedCourseId, searchKeyword);
});
it('should show an error state if any', () => {
mockModels({
errors: ['foo'],
});
renderComponent();
expect(screen.queryByTestId('courseware-search-error')).toBeInTheDocument();
});
it('should not show a summary if there are no results', () => {
mockModels({
searchKeyword: 'test',
total: 0,
});
renderComponent();
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
});
it('should show a summary for the results', () => {
mockModels({
searchKeyword: 'fubar',
total: 1,
});
renderComponent();
expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
});
});
describe('when clearing the search input', () => {
it('should clear the search by dispatch updateModel', async () => {
mockModels({
searchKeyword: 'fubar',
total: 2,
});
renderComponent();
await waitFor(() => {
const input = screen.queryByTestId('courseware-search-form').querySelector('input');
fireEvent.change(input, { target: { value: '' } });
});
expect(updateModel).toHaveBeenCalledWith({
modelType: 'contentSearchResults',
model: {
id: decodedCourseId,
searchKeyword: '',
results: [],
errors: undefined,
loading: false,
},
});
});
});
});

View File

@@ -1,24 +0,0 @@
import React from 'react';
import {
initializeMockApp,
render,
screen,
} from '../../setupTest';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
function renderComponent() {
const { container } = render(<CoursewareSearchEmpty />);
return container;
}
describe('CoursewareSearchEmpty', () => {
beforeAll(async () => {
initializeMockApp();
});
it('should match the snapshot', () => {
renderComponent();
expect(screen.getByTestId('no-results')).toMatchSnapshot();
});
});

View File

@@ -1,11 +1,8 @@
import React from 'react';
import { SearchField } from '@edx/paragon';
import PropTypes from 'prop-types';
import { SearchField } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
const CoursewareSearchForm = ({
intl,
searchTerm,
onSubmit,
onChange,
@@ -17,27 +14,17 @@ const CoursewareSearchForm = ({
onChange={onChange}
submitButtonLocation="external"
className="courseware-search-form"
screenReaderText={{
label: intl.formatMessage(messages.searchSubmitLabel),
clearButton: intl.formatMessage(messages.searchClearAction),
submitButton: null, // Remove the sr-only label in the button.
}}
>
<div className="pgn__searchfield_wrapper" data-testid="courseware-search-form">
<SearchField.Label />
<SearchField.Input placeholder={placeholder} autoFocus />
<SearchField.ClearButton />
</div>
<SearchField.SubmitButton
buttonText={intl.formatMessage(messages.searchSubmitLabel)}
submitButtonLocation="external"
data-testid="courseware-search-form-submit"
/>
<SearchField.SubmitButton submitButtonLocation="external" />
</SearchField.Advanced>
);
CoursewareSearchForm.propTypes = {
intl: intlShape.isRequired,
searchTerm: PropTypes.string,
onSubmit: PropTypes.func,
onChange: PropTypes.func,
@@ -51,4 +38,4 @@ CoursewareSearchForm.defaultProps = {
placeholder: undefined,
};
export default injectIntl(CoursewareSearchForm);
export default CoursewareSearchForm;

View File

@@ -47,8 +47,8 @@ describe('CoursewareSearchToggle', () => {
it('should call onSubmit handler when submit is clicked', async () => {
await act(async () => renderComponent(placeholderText, onSubmitHandlerMock, onChangeHandlerMock));
await waitFor(async () => {
const element = await screen.findByTestId('courseware-search-form-submit');
await waitFor(() => {
const element = screen.queryAllByText('Search')[0];
fireEvent.click(element);
expect(onSubmitHandlerMock).toHaveBeenCalledTimes(1);
});

View File

@@ -1,22 +1,21 @@
import React from 'react';
import {
Folder, TextFields, VideoCamera, Article,
} from '@openedx/paragon/icons';
} from '@edx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import { Icon } from '@openedx/paragon';
import { Icon } from '@edx/paragon';
import PropTypes from 'prop-types';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
const iconTypeMapping = {
document: Folder,
text: TextFields,
video: VideoCamera,
sequence: Folder,
other: Article,
};
const defaultIcon = Article;
const CoursewareSearchResults = ({ results = [] }) => {
const CoursewareSearchResults = ({ results }) => {
if (!results?.length) {
return <CoursewareSearchEmpty />;
}
@@ -35,7 +34,9 @@ const CoursewareSearchResults = ({ results = [] }) => {
}) => {
const key = type.toLowerCase();
const icon = iconTypeMapping[key] || defaultIcon;
const isExternal = !url.startsWith('/');
const linkProps = isExternal ? {
href: url,
target: '_blank',
@@ -69,13 +70,14 @@ const CoursewareSearchResults = ({ results = [] }) => {
};
CoursewareSearchResults.propTypes = {
results: PropTypes.arrayOf(PropTypes.shape({
results: PropTypes.arrayOf(PropTypes.objectOf({
id: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string,
location: PropTypes.arrayOf(PropTypes.string),
url: PropTypes.string,
contentHits: PropTypes.number,
score: PropTypes.number,
})),
};

View File

@@ -6,7 +6,7 @@ import {
} from '../../setupTest';
import CoursewareSearchResults from './CoursewareSearchResults';
import messages from './messages';
import searchResultsFactory from './test-data/search-results-factory';
// import mockedData from './test-data/mockedResults'; // TODO: Update this test.
jest.mock('react-redux');
@@ -28,14 +28,11 @@ describe('CoursewareSearchResults', () => {
});
});
describe('when list of results is provided', () => {
beforeEach(() => {
const { results } = searchResultsFactory('course');
renderComponent({ results });
});
/* describe('when list of results is provided', () => {
beforeEach(() => { renderComponent({ results: mockedData }); });
it('should match the snapshot', () => {
expect(screen.getByTestId('search-results')).toMatchSnapshot();
});
});
}); */
});

View File

@@ -1,26 +1,17 @@
import React, { useEffect } from 'react';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@openedx/paragon';
import { Search } from '@openedx/paragon/icons';
import { Button, Icon } from '@edx/paragon';
import { Search } from '@edx/paragon/icons';
import { useDispatch } from 'react-redux';
import messages from './messages';
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
import { setShowSearch } from '../data/slice';
import messages from './messages';
import { useCoursewareSearchFeatureFlag } from './hooks';
const CoursewareSearchToggle = ({
intl,
}) => {
const dispatch = useDispatch();
const enabled = useCoursewareSearchFeatureFlag();
const { query } = useCoursewareSearchParams();
const handleSearchOpenClick = () => {
dispatch(setShowSearch(true));
};
useEffect(() => {
if (enabled && !!query) { handleSearchOpenClick(); }
}, [enabled]);
if (!enabled) { return null; }
@@ -31,7 +22,7 @@ const CoursewareSearchToggle = ({
size="sm"
className="p-1 mt-2 mr-2 rounded-lg"
aria-label={intl.formatMessage(messages.searchOpenAction)}
onClick={handleSearchOpenClick}
onClick={() => dispatch(setShowSearch(true))}
data-testid="courseware-search-open-button"
>
<Icon src={Search} />

View File

@@ -12,33 +12,14 @@ import { setShowSearch } from '../data/slice';
import { CoursewareSearchToggle } from './index';
const mockDispatch = jest.fn();
const mockCoursewareSearchParams = jest.fn();
jest.mock('../data/thunks');
jest.mock('../data/slice');
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
jest.mock('./hooks', () => ({
...jest.requireActual('./hooks'),
useCoursewareSearchParams: () => mockCoursewareSearchParams,
}));
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const mockSearchParams = ((props = coursewareSearch) => {
mockCoursewareSearchParams.mockReturnValue(props);
});
function renderComponent() {
const { container } = render(<CoursewareSearchToggle />);
return container;
@@ -55,7 +36,6 @@ describe('CoursewareSearchToggle', () => {
it('Should not render when the waffle flag is disabled', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: false }));
mockSearchParams();
await act(async () => renderComponent());
await waitFor(() => {
@@ -66,8 +46,6 @@ describe('CoursewareSearchToggle', () => {
it('Should render when the waffle flag is enabled', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true }));
mockSearchParams();
await act(async () => renderComponent());
await waitFor(() => {
@@ -78,8 +56,6 @@ describe('CoursewareSearchToggle', () => {
it('Should dispatch setShowSearch(true) when clicking the search button', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true }));
mockSearchParams();
await act(async () => renderComponent());
const button = await screen.findByTestId('courseware-search-open-button');
fireEvent.click(button);

View File

@@ -1,10 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CoursewareSearchEmpty should match the snapshot 1`] = `
<p
class="courseware-search-results__empty"
data-testid="no-results"
>
No results found.
</p>
`;

View File

@@ -1,1238 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = `
<div
class="courseware-search-results"
data-testid="search-results"
>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 4H2v16h20V6H12l-2-2z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Demo Course Overview
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Passing a Course
</span>
<em>
1
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Passing a Course
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 4H2v16h20V6H12l-2-2z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Passing a Course
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Passing a Course
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Text Input
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Text input
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Pointing on a Picture
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Pointing on a Picture
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Getting Answers
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Getting Answers
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Welcome!
</span>
<em>
30
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
<li>
<div>
Introduction: Video and Sequences
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Multiple Choice Questions
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Multiple Choice Questions
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Numerical Input
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Numerical Input
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Connecting a Circuit and a Circuit Diagram
</span>
<em>
3
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Lesson 1 - Getting Started
</div>
</li>
<li>
<div>
Video Presentation Styles
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
CAPA
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 2: Get Interactive
</div>
</li>
<li>
<div>
Homework - Labs and Demos
</div>
</li>
<li>
<div>
Code Grader
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Interactive Questions
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Lesson 1 - Getting Started
</div>
</li>
<li>
<div>
Interactive Questions
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Blank HTML Page
</span>
<em>
6
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
<li>
<div>
Introduction: Video and Sequences
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Discussion Forums
</span>
<em>
5
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Lesson 3 - Be Social
</div>
</li>
<li>
<div>
Discussion Forums
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Overall Grade
</span>
<em>
7
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Overall Grade Performance
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Blank HTML Page
</span>
<em>
3
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Lesson 3 - Be Social
</div>
</li>
<li>
<div>
Homework - Find Your Study Buddy
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Find Your Study Buddy
</span>
<em>
3
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Homework - Find Your Study Buddy
</div>
</li>
<li>
<div>
Homework - Find Your Study Buddy
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Be Social
</span>
<em>
4
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Lesson 3 - Be Social
</div>
</li>
<li>
<div>
Be Social
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
EdX Exams
</span>
<em>
4
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
EdX Exams
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
When Are Your Exams?
</span>
<em>
2
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Lesson 1 - Getting Started
</div>
</li>
<li>
<div>
When Are Your Exams?
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="https://www.edx.org"
rel="nofollow"
target="_blank"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
External Course Link Test
</span>
</div>
</div>
</a>
</div>
`;

View File

@@ -18,11 +18,6 @@ Object {
"key": "text",
"label": "Text",
},
Object {
"count": 1,
"key": "unknown",
"label": "Unknown",
},
Object {
"count": 2,
"key": "video",
@@ -291,15 +286,6 @@ Object {
"type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
},
Object {
"contentHits": 0,
"id": "random-element-id",
"location": null,
"score": 0.82610154,
"title": "External Course Link Test",
"type": "unknown",
"url": "https://www.edx.org",
},
],
"total": 29,
}

View File

@@ -6,7 +6,7 @@
right: 0;
bottom: 0;
border-top: 1px solid $light-300;
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
z-index: 200;
&__close {
position: absolute !important; // For some reason it gets overridden
@@ -35,7 +35,7 @@
&__results-summary {
font-size: .9rem;
color: $gray-500;
color: $gray-400;
padding: 1rem 0 .5rem;
}
@@ -51,8 +51,6 @@
&__empty {
color: $gray-500;
padding: 6rem 0;
text-align: center;
}
&__item {
@@ -113,7 +111,7 @@
&__breadcrumbs {
display: flex;
gap: 1.25rem;
color: $gray-500;
color: $gray-400;
overflow: hidden;
list-style: none;
padding: 0;
@@ -143,14 +141,6 @@
}
}
.courseware-search-results-tabs {
border-bottom-color: $gray-400 !important;
&.nav-tabs .nav-link.active {
border-bottom-width: 4px !important;
}
}
@media (min-width: map-get($grid-breakpoints, 'md')) {
.courseware-search__content {
padding-top: 8rem;

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useLayoutEffect } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { debounce } from 'lodash';
import { fetchCoursewareSearchSettings } from '../data/thunks';
@@ -69,19 +69,3 @@ export function useLockScroll() {
};
}, []);
}
const initSearchParams = { q: '', f: '' };
export function useCoursewareSearchParams() {
const [searchParams, setSearchParams] = useSearchParams(initSearchParams);
const clearSearchParams = () => setSearchParams(initSearchParams);
const query = searchParams.get('q');
const filter = searchParams.get('f')?.toLowerCase();
const setQuery = (q) => setSearchParams((params) => ({ q, f: params.get('f') }));
const setFilter = (f) => setSearchParams((params) => ({ q: params.get('q'), f }));
return {
query, filter, setQuery, setFilter, clearSearchParams,
};
}

View File

@@ -1,13 +1,9 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useParams, useSearchParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { fetchCoursewareSearchSettings } from '../data/thunks';
import {
useCoursewareSearchFeatureFlag,
useCoursewareSearchParams,
useCoursewareSearchState,
useElementBoundingBox,
useLockScroll,
useCoursewareSearchFeatureFlag, useCoursewareSearchState, useElementBoundingBox, useLockScroll,
} from './hooks';
jest.mock('react-redux');
@@ -188,45 +184,4 @@ describe('CoursewareSearch Hooks', () => {
expect(removeBodyClassSpy).toHaveBeenCalledWith('_search-no-scroll');
});
});
describe('useSearchParams', () => {
const initSearch = { q: '', f: '' };
const q = { value: '' };
const f = { value: '' };
const mockedQuery = { q, f };
const searchParams = { get: (prop) => mockedQuery[prop].value };
const setSearchParams = jest.fn();
beforeEach(() => {
useSearchParams.mockImplementation(() => [searchParams, setSearchParams]);
});
it('should init the search params properly', () => {
const {
query, filter, setQuery, setFilter, clearSearchParams,
} = useCoursewareSearchParams();
expect(useSearchParams).toBeCalledWith(initSearch);
expect(query).toBe('');
expect(filter).toBe('');
setQuery('setQuery');
expect(setSearchParams).toBeCalledWith(expect.any(Function));
setFilter('setFilter');
expect(setSearchParams).toBeCalledWith(expect.any(Function));
clearSearchParams();
expect(setSearchParams).toBeCalledWith(initSearch);
});
it('should return the query and lowercase filter if any', () => {
q.value = '42';
f.value = 'LOWERCASE';
const { query, filter } = useCoursewareSearchParams();
expect(query).toBe('42');
expect(filter).toBe('lowercase');
});
});
});

View File

@@ -1,8 +1,8 @@
const Joi = require('joi');
const endpointSchema = Joi.object({
took: Joi.number().required(),
total: Joi.number().required(),
took: Joi.number(),
total: Joi.number(),
maxScore: Joi.number().allow(null),
results: Joi.array().items(Joi.object({
id: Joi.string(),

View File

@@ -19,7 +19,6 @@ describe('mapSearchResponse', () => {
{ key: 'capa', label: 'CAPA', count: 7 },
{ key: 'sequence', label: 'Sequence', count: 2 },
{ key: 'text', label: 'Text', count: 9 },
{ key: 'unknown', label: 'Unknown', count: 1 },
{ key: 'video', label: 'Video', count: 2 },
];
expect(response.filters).toEqual(expectedFilters);

View File

@@ -6,16 +6,6 @@ const messages = defineMessages({
defaultMessage: 'Search within this course',
description: 'Aria-label for a button that will pop up Courseware Search.',
},
searchSubmitLabel: {
id: 'learn.coursewareSerch.submitLabel',
defaultMessage: 'Search',
description: 'Button label that will submit Courseware Search.',
},
searchClearAction: {
id: 'learn.coursewareSerch.clearAction',
defaultMessage: 'Clear search',
description: 'Button label that will the current Courseware Search input.',
},
searchCloseAction: {
id: 'learn.coursewareSerch.closeAction',
defaultMessage: 'Close the search form',
@@ -41,10 +31,15 @@ const messages = defineMessages({
defaultMessage: 'No results found.',
description: 'Text to show when the Courseware Search found no results matching the criteria.',
},
searchResultsLabel: {
id: 'learn.coursewareSerch.searchResultsLabel',
defaultMessage: 'Results for "{keyword}":',
description: 'Text to show above the search results response list.',
searchResultsSingular: {
id: 'learn.coursewareSerch.searchResultsSingular',
defaultMessage: '1 match found for "{keyword}":',
description: 'Text to show when the Courseware Search found only one result matching the criteria.',
},
searchResultsPlural: {
id: 'learn.coursewareSerch.searchResultsPlural',
defaultMessage: '{total} matches found for "{keyword}":',
description: 'Text to show when the Courseware Search found multiple results matching the criteria.',
},
searchResultsError: {
id: 'learn.coursewareSerch.searchResultsError',
@@ -53,8 +48,8 @@ const messages = defineMessages({
},
// These are translations for labeling the filters
'filter:all': {
id: 'learn.coursewareSerch.filter:all',
'filter:none': {
id: 'learn.coursewareSerch.filter:none',
defaultMessage: 'All content',
description: 'Label for the search results filter that shows all content (no filter).',
},
@@ -68,16 +63,6 @@ const messages = defineMessages({
defaultMessage: 'Video',
description: 'Label for the search results filter that shows results with video content.',
},
'filter:sequence': {
id: 'learn.coursewareSerch.filter:sequence',
defaultMessage: 'Section',
description: 'Label for the search results filter that shows results with section content.',
},
'filter:other': {
id: 'learn.coursewareSerch.filter:other',
defaultMessage: 'Other',
description: 'Label for the search results filter that shows results with other content.',
},
});
export default messages;

View File

@@ -542,28 +542,6 @@
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf"
},
"score": 0.82610154
},
{
"_index": "courseware_content",
"_type": "_doc",
"_id": "some-external-reference",
"data": {
"course": "External Course",
"org": "edX",
"content": {
"display_name": "External Course Link Test",
"html_content": "This should open a new tab when following the link."
},
"content_type": "Unknown",
"id": "random-element-id",
"start_date": "2013-02-05T05:00:00+00:00",
"content_groups": null,
"course_name": "Demonstration Course",
"location": null,
"excerpt": "Just testing external links",
"url": "https://www.edx.org"
},
"score": 0.82610154
}
],
"access_denied_count": 0

View File

@@ -1,17 +0,0 @@
import { camelCaseObject } from '@edx/frontend-platform';
import mockedData from './mocked-response.json';
import mapSearchResponse from '../map-search-response';
function searchResultsFactory(searchKeywords = '', moreInfo = {}) {
const data = camelCaseObject(mockedData);
const info = mapSearchResponse(data, searchKeywords);
const result = {
...info,
...moreInfo,
};
return result;
}
export default searchResultsFactory;

View File

@@ -6,7 +6,6 @@ Factory.define('courseHomeMetadata')
.option('host', 'http://localhost:18000')
.attrs({
title: 'Demonstration Course',
is_new_discussion_sidebar_view_enabled: false,
is_self_paced: false,
is_enrolled: false,
is_staff: false,

View File

@@ -14,11 +14,7 @@ Object {
},
"courseware": Object {
"courseId": null,
"courseOutline": Object {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": Object {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
@@ -42,7 +38,6 @@ Object {
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
@@ -314,74 +309,9 @@ Object {
},
},
},
"plugins": Object {},
"recommendations": Object {
"recommendationsStatus": "loading",
},
"specialExams": Object {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": Object {
"attempt": Object {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": Object {
"are_prerequisites_satisifed": true,
"declined_prerequisites": Array [],
"failed_prerequisites": Array [],
"pending_prerequisites": Array [],
"satisfied_prerequisites": Array [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": Object {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": Object {
"exam_proctoring_backend": Object {
"download_url": "",
"instructions": Array [],
"name": "",
"rules": Object {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": Object {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,
@@ -406,11 +336,7 @@ Object {
},
"courseware": Object {
"courseId": null,
"courseOutline": Object {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": Object {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
@@ -434,7 +360,6 @@ Object {
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
@@ -510,7 +435,6 @@ Object {
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
"complete": false,
"courseId": "course-v1:edX+DemoX+Demo_Course",
"hideFromTOC": undefined,
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"resumeBlock": false,
"sequenceIds": Array [
@@ -526,10 +450,8 @@ Object {
"due": null,
"effortActivities": 2,
"effortTime": 15,
"hideFromTOC": undefined,
"icon": null,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
"navigationDisabled": undefined,
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"showLink": true,
"title": "Title of Sequence",
@@ -587,74 +509,9 @@ Object {
},
},
},
"plugins": Object {},
"recommendations": Object {
"recommendationsStatus": "loading",
},
"specialExams": Object {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": Object {
"attempt": Object {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": Object {
"are_prerequisites_satisifed": true,
"declined_prerequisites": Array [],
"failed_prerequisites": Array [],
"pending_prerequisites": Array [],
"satisfied_prerequisites": Array [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": Object {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": Object {
"exam_proctoring_backend": Object {
"download_url": "",
"instructions": Array [],
"name": "",
"rules": Object {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": Object {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,
@@ -679,11 +536,7 @@ Object {
},
"courseware": Object {
"courseId": null,
"courseOutline": Object {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": Object {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
@@ -707,7 +560,6 @@ Object {
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
@@ -863,74 +715,9 @@ Object {
},
},
},
"plugins": Object {},
"recommendations": Object {
"recommendationsStatus": "loading",
},
"specialExams": Object {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": Object {
"attempt": Object {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": Object {
"are_prerequisites_satisifed": true,
"declined_prerequisites": Array [],
"failed_prerequisites": Array [],
"pending_prerequisites": Array [],
"satisfied_prerequisites": Array [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": Object {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": Object {
"exam_proctoring_backend": Object {
"download_url": "",
"instructions": Array [],
"name": "",
"rules": Object {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": Object {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,

View File

@@ -136,7 +136,6 @@ export function normalizeOutlineBlocks(courseId, blocks) {
title: block.display_name,
resumeBlock: block.resume_block,
sequenceIds: block.children || [],
hideFromTOC: block.hide_from_toc,
};
break;
@@ -153,8 +152,6 @@ export function normalizeOutlineBlocks(courseId, blocks) {
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
showLink: !!block.lms_web_url,
title: block.display_name,
hideFromTOC: block.hide_from_toc,
navigationDisabled: block.navigation_disabled,
};
break;

View File

@@ -55,29 +55,6 @@ describe('Data layer integration tests', () => {
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('should result in fetch failed if course metadata call errored', async () => {
const datesTabData = Factory.build('datesTabData');
const datesUrl = `${datesBaseUrl}/${courseId}`;
axiosMock.onGet(courseMetadataUrl).networkError();
axiosMock.onGet(datesUrl).reply(200, datesTabData);
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('should result in fetch failed if course metadata call errored', async () => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).networkError();
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('Should fetch, normalize, and save metadata', async () => {
const datesTabData = Factory.build('datesTabData');
@@ -101,14 +78,18 @@ describe('Data layer integration tests', () => {
});
it.each([401, 403, 404])(
'should result in fetch denied if course access is denied, regardless of dates API status',
'should result in fetch denied for expected errors and failed for all others',
async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).reply(errorStatus, {});
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(store.getState().courseHome.courseStatus).toEqual('denied');
let expectedState = 'failed';
if (errorStatus === 401 || errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
},
);
});
@@ -148,14 +129,18 @@ describe('Data layer integration tests', () => {
});
it.each([401, 403, 404])(
'should result in fetch denied if course access is denied, regardless of outline API status',
'should result in fetch denied for expected errors and failed for all others',
async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(outlineUrl).reply(errorStatus, {});
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
expect(store.getState().courseHome.courseStatus).toEqual('denied');
let expectedState = 'failed';
if (errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
},
);
});

View File

@@ -38,41 +38,28 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) {
return async (dispatch) => {
dispatch(fetchTabRequest({ courseId }));
try {
const promisesToFulfill = [getCourseHomeCourseMetadata(courseId, 'outline')];
if (getTabData) {
promisesToFulfill.push(getTabData(courseId, targetUserId));
}
const [
courseHomeCourseMetadataResult,
tabDataResult,
] = await Promise.allSettled(promisesToFulfill);
if (courseHomeCourseMetadataResult.status === 'fulfilled') {
dispatch(addModel({
modelType: 'courseHomeMeta',
model: {
id: courseId,
...courseHomeCourseMetadataResult.value,
},
}));
}
if (tabDataResult?.status === 'fulfilled') {
const courseHomeCourseMetadata = await getCourseHomeCourseMetadata(courseId, 'outline');
dispatch(addModel({
modelType: 'courseHomeMeta',
model: {
id: courseId,
...courseHomeCourseMetadata,
},
}));
const tabDataResult = getTabData && await getTabData(courseId, targetUserId);
if (tabDataResult) {
dispatch(addModel({
modelType: tab,
model: {
id: courseId,
...tabDataResult.value,
...tabDataResult,
},
}));
}
if (courseHomeCourseMetadataResult.status === 'rejected') {
throw courseHomeCourseMetadataResult.reason;
} else if (!courseHomeCourseMetadataResult.value.courseAccess.hasAccess) {
// If the learner does not have access to the course, short cut to dispatch to a denied response regardless of
// the tabDataResult.
// Disable the access-denied path for now - it caused a regression
if (!courseHomeCourseMetadata.courseAccess.hasAccess) {
dispatch(fetchTabDenied({ courseId }));
} else if (tabDataResult?.status === 'rejected') {
throw tabDataResult.reason;
} else {
} else if (tabDataResult || !getTabData) {
dispatch(fetchTabSuccess({
courseId,
targetUserId,

View File

@@ -8,7 +8,7 @@ import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
import { Tooltip, OverlayTrigger } from '@edx/paragon';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { Badge } from '@openedx/paragon';
import { Badge } from '@edx/paragon';
import messages from '../messages';
import { daycmp, isLearnerAssignment } from '../utils';
@@ -38,21 +38,21 @@ function getBadgeListAndColor(date, intl, item, items) {
message: messages.today,
shownForDay: isToday,
bg: 'bg-warning-300',
className: 'text-dark',
className: 'text-black',
},
{
message: messages.completed,
shownForDay: assignments.length && assignments.every(isComplete),
shownForItem: x => isLearnerAssignment(x) && isComplete(x),
bg: 'bg-light-500',
className: 'text-dark',
className: 'text-black',
},
{
message: messages.pastDue,
shownForDay: assignments.length && assignments.every(isPastDue),
shownForItem: x => isLearnerAssignment(x) && isPastDue(x),
bg: 'bg-dark-200',
className: 'text-dark',
className: 'text-white',
},
{
message: messages.dueNext,

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink } from '@openedx/paragon';
import { Button, Hyperlink } from '@edx/paragon';
import messages from './messages';
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';

View File

@@ -9,9 +9,8 @@ const LmsHtmlFragment = ({
title,
...rest
}) => {
const direction = document.documentElement?.getAttribute('dir') || 'ltr';
const wholePage = `
<html dir="${direction}">
<html>
<head>
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
@@ -30,7 +29,7 @@ const LmsHtmlFragment = ({
const iframe = useRef(null);
function resetIframeHeight() {
if (iframe?.current?.contentWindow?.document?.body) {
iframe.current.height = iframe.current.contentWindow.document.body.parentNode.scrollHeight;
iframe.current.height = iframe.current.contentWindow.document.body.scrollHeight;
}
}

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import { AlertList } from '../../generic/user-messages';
import CourseDates from './widgets/CourseDates';

View File

@@ -23,26 +23,8 @@ import { CERT_STATUS_TYPE } from './alerts/certificate-status-alert/CertificateS
import OutlineTab from './OutlineTab';
import LoadedTabPage from '../../tab-page/LoadedTabPage';
const mockCoursewareSearchParams = jest.fn();
initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
jest.mock('../courseware-search/hooks', () => ({
...jest.requireActual('../courseware-search/hooks'),
useCoursewareSearchParams: () => mockCoursewareSearchParams,
}));
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const mockSearchParams = ((props = coursewareSearch) => {
mockCoursewareSearchParams.mockReturnValue(props);
});
describe('Outline Tab', () => {
let axiosMock;
@@ -95,16 +77,9 @@ describe('Outline Tab', () => {
expiration_date: null,
});
// Mock courseware search params
mockSearchParams();
logUnhandledRequests(axiosMock);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Course Outline', () => {
it('displays link to start course', async () => {
await fetchAndRender();
@@ -282,22 +257,6 @@ describe('Outline Tab', () => {
});
});
it('ignores comments and misformatted HTML', async () => {
setTabData({
welcome_message_html: '<p class="additional-spaces-in-tag" >'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ 'Test welcome message that happens to be longer than one hundred words because of comments but displayed content is less.'
+ 'It should not be shortened.'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '</p>',
});
await fetchAndRender();
const showMoreButton = screen.queryByRole('button', { name: 'Show More' });
expect(showMoreButton).not.toBeInTheDocument();
});
it('does not display if no update available', async () => {
setTabData({ welcome_message_html: null });
await fetchAndRender();
@@ -1285,97 +1244,5 @@ describe('Outline Tab', () => {
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl);
});
it('section should show hidden from toc message when hide_from_toc is true', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: true,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-icon');
const textHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-text');
expect(iconHiddenFromTocSectionNode).toBeInTheDocument();
expect(textHiddenFromTocSectionNode).toBeInTheDocument();
expect(textHiddenFromTocSectionNode.textContent).toBe('Hidden in Course Outline, accessible via link');
});
it('section should not show hidden from toc message when hide_from_toc is false', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: false,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-icon');
const textHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-text');
expect(iconHiddenFromTocSectionNode).not.toBeInTheDocument();
expect(textHiddenFromTocSectionNode).not.toBeInTheDocument();
});
it('sequence link should show hidden from toc message when hide_from_toc is true', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: true,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSequenceLinkNode = screen.getByTestId('hide-from-toc-sequence-link-icon');
const textHiddenFromTocSequenceLink = screen.getByTestId('hide-from-toc-sequence-link-text');
expect(iconHiddenFromTocSequenceLinkNode).toBeInTheDocument();
expect(textHiddenFromTocSequenceLink).toBeInTheDocument();
expect(textHiddenFromTocSequenceLink.textContent).toBe('Subsections are not navigable between each other, they can only be accessed through their link.');
});
it('sequence link not show hidden from toc message when hide_from_toc is false', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: false,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-icon');
const textHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-text');
expect(iconHiddenFromTocSequenceLink).not.toBeInTheDocument();
expect(textHiddenFromTocSequenceLink).not.toBeInTheDocument();
});
});
});

View File

@@ -1,12 +1,11 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible, IconButton, Icon } from '@openedx/paragon';
import { Collapsible, IconButton } from '@edx/paragon';
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { DisabledVisible } from '@openedx/paragon/icons';
import SequenceLink from './SequenceLink';
import { useModel } from '../../generic/model-store';
@@ -24,7 +23,6 @@ const Section = ({
complete,
sequenceIds,
title,
hideFromTOC,
} = section;
const {
courseBlocks: {
@@ -44,7 +42,7 @@ const Section = ({
}, []);
const sectionTitle = (
<div className="d-flex row w-100 m-0">
<div className="row w-100 m-0">
<div className="col-auto p-0">
{complete ? (
<FontAwesomeIcon
@@ -64,24 +62,12 @@ const Section = ({
/>
)}
</div>
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500">
<span className="align-middle col-6">{title}</span>
<div className="col-10 ml-3 p-0 font-weight-bold text-dark-500">
<span className="align-middle">{title}</span>
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
</span>
</div>
{hideFromTOC && (
<div className="row">
{hideFromTOC && (
<span className="small d-flex align-content-end">
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
<span data-testid="hide-from-toc-section-text">
{intl.formatMessage(messages.hiddenSection)}
</span>
</span>
)}
</div>
)}
</div>
);

View File

@@ -1,3 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
@@ -11,8 +12,6 @@ import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-ico
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Icon } from '@openedx/paragon';
import { Block } from '@openedx/paragon/icons';
import EffortEstimate from '../../shared/effort-estimate';
import { useModel } from '../../generic/model-store';
import messages from './messages';
@@ -30,7 +29,6 @@ const SequenceLink = ({
due,
showLink,
title,
hideFromTOC,
} = sequence;
const {
userTimezone,
@@ -95,7 +93,7 @@ const SequenceLink = ({
icon={fasCheckCircle}
fixedWidth
className="float-left text-success mt-1"
aria-hidden={complete}
aria-hidden="true"
title={intl.formatMessage(messages.completedAssignment)}
/>
) : (
@@ -103,7 +101,7 @@ const SequenceLink = ({
icon={farCheckCircle}
fixedWidth
className="float-left text-gray-400 mt-1"
aria-hidden={complete}
aria-hidden="true"
title={intl.formatMessage(messages.incompleteAssignment)}
/>
)}
@@ -116,16 +114,6 @@ const SequenceLink = ({
<EffortEstimate className="ml-3 align-middle" block={sequence} />
</div>
</div>
{hideFromTOC && (
<div className="row w-100 my-2 mx-4 pl-3">
<span className="small d-flex">
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
<span data-testid="hide-from-toc-sequence-link-text">
{intl.formatMessage(messages.hiddenSequenceLink)}
</span>
</span>
</div>
)}
<div className="row w-100 m-0 ml-3 pl-3">
<small className="text-body pl-2">
{due ? dueDateMessage : noDueDateMessage}

View File

@@ -6,7 +6,7 @@ import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@openedx/paragon';
import { Alert, Button } from '@edx/paragon';
import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -6,8 +6,8 @@ import {
FormattedRelativeTime,
FormattedTime,
} from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
const DAY_SEC = 24 * 60 * 60; // in seconds
const DAY_MS = DAY_SEC * 1000; // in ms

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Alert, Button, Hyperlink } from '@openedx/paragon';
import { Alert, Button, Hyperlink } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';

View File

@@ -1,5 +1,5 @@
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@openedx/paragon';
import { Alert, Button } from '@edx/paragon';
import React from 'react';
import PropTypes from 'prop-types';

View File

@@ -36,16 +36,6 @@ const messages = defineMessages({
defaultMessage: 'Completed section',
description: 'Text used to describe the green checkmark icon in front of a section title',
},
hiddenSection: {
id: 'learning.outline.hiddenSection',
defaultMessage: 'Hidden in Course Outline, accessible via link',
description: 'Label for hidden section in course outline',
},
hiddenSequenceLink: {
id: 'learning.outline.hiddenSequenceLink',
defaultMessage: 'Subsections are not navigable between each other, they can only be accessed through their link.',
description: 'Label for hidden sequence in course outline',
},
dates: {
id: 'learning.outline.dates',
defaultMessage: 'Important dates',

View File

@@ -1,5 +1,5 @@
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
.flag-button {

View File

@@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import camelCase from 'lodash.camelcase';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import messages from '../messages';
import { getProctoringInfoData } from '../../data/api';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Button, Card } from '@openedx/paragon';
import { Button, Card } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';

View File

@@ -2,12 +2,12 @@ import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Form, Card, Icon } from '@openedx/paragon';
import { Form, Card, Icon } from '@edx/paragon';
import { history } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Email } from '@openedx/paragon/icons';
import { Email } from '@edx/paragon/icons';
import { useSelector } from 'react-redux';
import messages from '../messages';
import LearningGoalButton from './LearningGoalButton';

View File

@@ -1,8 +1,8 @@
import React, { useState, useMemo } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert, Button, TransitionReplace } from '@openedx/paragon';
import { Alert, Button, TransitionReplace } from '@edx/paragon';
import truncate from 'truncate-html';
import { useDispatch } from 'react-redux';
@@ -18,22 +18,8 @@ const WelcomeMessage = ({ courseId, intl }) => {
const [display, setDisplay] = useState(true);
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
// messageCanBeShortened. We clean it by calling truncate with a length of welcomeMessageHtml.length which
// will not result in a truncation but a formatting into 'truncate-html' canonical format.
const cleanedWelcomeMessageHtml = useMemo(
() => truncate(welcomeMessageHtml, welcomeMessageHtml.length, { keepWhitespaces: true }),
[welcomeMessageHtml],
);
const shortWelcomeMessageHtml = useMemo(
() => truncate(cleanedWelcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true }),
[cleanedWelcomeMessageHtml],
);
const messageCanBeShortened = useMemo(
() => (shortWelcomeMessageHtml.length < cleanedWelcomeMessageHtml.length),
[cleanedWelcomeMessageHtml, shortWelcomeMessageHtml],
);
const shortWelcomeMessageHtml = truncate(welcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true });
const messageCanBeShortened = shortWelcomeMessageHtml.length < welcomeMessageHtml.length;
const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened);
const dispatch = useDispatch();
@@ -77,7 +63,7 @@ const WelcomeMessage = ({ courseId, intl }) => {
className="inline-link"
data-testid="long-welcome-message-iframe"
key="full-html"
html={cleanedWelcomeMessageHtml}
html={welcomeMessageHtml}
title={intl.formatMessage(messages.welcomeMessage)}
/>
)}

View File

@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import { useModel } from '../../generic/model-store';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { breakpoints, useWindowSize } from '@edx/paragon';
import CertificateStatus from './certificate-status/CertificateStatus';
import CourseCompletion from './course-completion/CourseCompletion';

View File

@@ -3,7 +3,7 @@ import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@openedx/paragon';
import { breakpoints } from '@edx/paragon';
import MockAdapter from 'axios-mock-adapter';
import {
@@ -16,26 +16,8 @@ import ProgressTab from './ProgressTab';
import LoadedTabPage from '../../tab-page/LoadedTabPage';
import messages from './grades/messages';
const mockCoursewareSearchParams = jest.fn();
initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
jest.mock('../courseware-search/hooks', () => ({
...jest.requireActual('../courseware-search/hooks'),
useCoursewareSearchParams: () => mockCoursewareSearchParams,
}));
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const mockSearchParams = ((props = coursewareSearch) => {
mockCoursewareSearchParams.mockReturnValue(props);
});
describe('Progress Tab', () => {
let axiosMock;
@@ -76,16 +58,9 @@ describe('Progress Tab', () => {
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
// Mock courseware search params
mockSearchParams();
logUnhandledRequests(axiosMock);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Related links', () => {
beforeEach(() => {
sendTrackEvent.mockClear();

View File

@@ -6,7 +6,7 @@ import {
FormattedDate, FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { Button, Card } from '@openedx/paragon';
import { Button, Card } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useModel } from '../../../generic/model-store';
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
@@ -154,7 +154,7 @@ const CertificateStatus = ({ intl }) => {
certAvailabilityDate = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
body = (
<FormattedMessage
id="progress.certificateStatus.notAvailable.endDate"
id="courseCelebration.certificateBody.notAvailable.endDate"
defaultMessage="This course ends on {endDate}. Final grades and any earned certificates are
scheduled to be available after {certAvailabilityDate}."
description="This shown for leaner when they are eligible for certifcate but it't not available yet, it could because leaners just finished the course quickly!"

View File

@@ -56,6 +56,11 @@ const messages = defineMessages({
defaultMessage: 'Your certificate is available!',
description: 'Header text when the certifcate is available',
},
downloadableBody: {
id: 'progress.certificateStatus.downloadableBody',
defaultMessage: 'Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.',
description: 'Recommending an action for learner when course certificate is available',
},
viewableButton: {
id: 'progress.certificateStatus.viewableButton',
defaultMessage: 'View my certificate',

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import { OverlayTrigger, Popover } from '@edx/paragon';
import messages from './messages';
@@ -33,7 +33,7 @@ const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) =>
show={showCompletePopover}
placement="top"
overlay={(
<Popover id="complete-content-tooltip-popover" aria-hidden="true">
<Popover aria-hidden="true">
<Popover.Content>
{intl.formatMessage(messages.completeContentTooltip)}
</Popover.Content>

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import { OverlayTrigger, Popover } from '@edx/paragon';
import messages from './messages';
@@ -37,7 +37,7 @@ const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
show={showIncompletePopover}
placement="top"
overlay={(
<Popover id="incomplete-tooltip-popover" aria-hidden="true">
<Popover aria-hidden="true">
<Popover.Content>
{intl.formatMessage(messages.incompleteContentTooltip)}
</Popover.Content>

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import { OverlayTrigger, Popover } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
@@ -36,7 +36,7 @@ const LockedDonutSegment = ({ intl, lockedPercentage }) => {
show={showLockedPopover}
placement="top"
overlay={(
<Popover id="locked-tooltip-popover" aria-hidden="true">
<Popover aria-hidden="true">
<Popover.Content>
{intl.formatMessage(messages.lockedContentTooltip)}
</Popover.Content>

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { CheckCircle, WarningFilled, WatchFilled } from '@openedx/paragon/icons';
import { Hyperlink, Icon } from '@openedx/paragon';
import { CheckCircle, WarningFilled, WatchFilled } from '@edx/paragon/icons';
import { Hyperlink, Icon } from '@edx/paragon';
import { useModel } from '../../../generic/model-store';
import { DashboardLink } from '../../../shared/links';

View File

@@ -3,8 +3,8 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { CheckCircle, WarningFilled } from '@openedx/paragon/icons';
import { breakpoints, Icon, useWindowSize } from '@openedx/paragon';
import { CheckCircle, WarningFilled } from '@edx/paragon/icons';
import { breakpoints, Icon, useWindowSize } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import GradeRangeTooltip from './GradeRangeTooltip';

View File

@@ -4,8 +4,8 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Locked } from '@openedx/paragon/icons';
import { Button, Icon } from '@openedx/paragon';
import { Locked } from '@edx/paragon/icons';
import { Button, Icon } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import { OverlayTrigger, Popover } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';

View File

@@ -3,10 +3,10 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { InfoOutline } from '@openedx/paragon/icons';
import { InfoOutline } from '@edx/paragon/icons';
import {
Icon, IconButton, OverlayTrigger, Popover,
} from '@openedx/paragon';
} from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@openedx/paragon';
import { OverlayTrigger, Popover } from '@edx/paragon';
import messages from '../messages';

View File

@@ -4,8 +4,8 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Blocked } from '@openedx/paragon/icons';
import { Icon, Hyperlink } from '@openedx/paragon';
import { Blocked } from '@edx/paragon/icons';
import { Icon, Hyperlink } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import DetailedGradesTable from './DetailedGradesTable';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { DataTable } from '@openedx/paragon';
import { DataTable } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -15,9 +15,8 @@ const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
<span id="problem-score-label" className="col-auto p-0">{intl.formatMessage(messages.problemScoreLabel)}</span>
<div className={classNames('col', 'p-0', { 'greyed-out': !subsection.learnerHasAccess })}>
<ul className="list-unstyled row w-100 m-0" aria-labelledby="problem-score-label">
{problemScores.map((problemScore, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={i} className="ml-3">{problemScore.earned}{isLocaleRtl ? '\\' : '/'}{problemScore.possible}</li>
{problemScores.map(problemScore => (
<li className="ml-3">{problemScore.earned}{isLocaleRtl ? '\\' : '/'}{problemScore.possible}</li>
))}
</ul>
</div>

View File

@@ -5,10 +5,10 @@ import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible, Icon, Row } from '@openedx/paragon';
import { Collapsible, Icon, Row } from '@edx/paragon';
import {
ArrowDropDown, ArrowDropUp, Blocked, Info,
} from '@openedx/paragon/icons';
} from '@edx/paragon/icons';
import messages from '../messages';
import { useModel } from '../../../../generic/model-store';

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Blocked } from '@openedx/paragon/icons';
import { Icon } from '@openedx/paragon';
import { Blocked } from '@edx/paragon/icons';
import { Icon } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -5,8 +5,8 @@ import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Icon, IconButton, OverlayTrigger, Popover,
} from '@openedx/paragon';
import { Blocked, InfoOutline } from '@openedx/paragon/icons';
} from '@edx/paragon';
import { Blocked, InfoOutline } from '@edx/paragon/icons';
import messages from '../messages';
import { useModel } from '../../../../generic/model-store';

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { DataTable } from '@openedx/paragon';
import { DataTable } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import AssignmentTypeCell from './AssignmentTypeCell';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
import { DataTable } from '@openedx/paragon';
import { DataTable } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { Hyperlink } from '@edx/paragon';
import messages from './messages';
import { useModel } from '../../../generic/model-store';

View File

@@ -8,7 +8,7 @@ import {
Button,
Row,
Col,
} from '@openedx/paragon';
} from '@edx/paragon';
import { resetDeadlines } from '../data';
import { useModel } from '../../generic/model-store';

View File

@@ -7,7 +7,7 @@ import {
Button,
Col,
Row,
} from '@openedx/paragon';
} from '@edx/paragon';
import { useModel } from '../../generic/model-store';
import messages from './messages';

View File

@@ -8,7 +8,7 @@ import {
Button,
Row,
Col,
} from '@openedx/paragon';
} from '@edx/paragon';
import { useModel } from '../../generic/model-store';
import messages from './messages';

View File

@@ -15,6 +15,9 @@ const CourseTabsNavigation = ({
return (
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
<div className="float-right">
<CoursewareSearchToggle />
</div>
<div className="container-xl">
<Tabs
className="nav-underline-tabs"
@@ -31,9 +34,6 @@ const CourseTabsNavigation = ({
))}
</Tabs>
</div>
<div className="course-tabs-navigation__search-toggle">
<CoursewareSearchToggle />
</div>
{show && <CoursewareSearch />}
</div>
);

View File

@@ -3,20 +3,13 @@ import { AppProvider } from '@edx/frontend-platform/react';
import {
initializeMockApp, render, screen,
} from '../setupTest';
import { useCoursewareSearchState, useCoursewareSearchParams } from '../course-home/courseware-search/hooks';
import { useCoursewareSearchState } from '../course-home/courseware-search/hooks';
import { CourseTabsNavigation } from './index';
import initializeStore from '../store';
jest.mock('../course-home/courseware-search/hooks');
const mockDispatch = jest.fn();
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
@@ -40,7 +33,6 @@ describe('Course Tabs Navigation', () => {
beforeEach(() => {
useCoursewareSearchState.mockImplementation(() => ({ show: false }));
useCoursewareSearchParams.mockReturnValue(coursewareSearch);
});
afterEach(() => {

View File

@@ -1,24 +0,0 @@
.course-tabs-navigation {
position: relative;
border-bottom: solid 1px #eaeaea;
.nav a,
.nav button {
&:hover {
background-color: $light-400;
}
}
.nav a {
&:not(.active):hover {
background-color: $light-400;
border-bottom: none;
}
}
&__search-toggle {
position: absolute;
top: .05rem;
right: 0;
}
}

View File

@@ -88,7 +88,6 @@ describe('CoursewareContainer', () => {
<Routes>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
key={route}
path={route}
element={<CoursewareContainer />}
/>
@@ -516,12 +515,5 @@ describe('CoursewareContainer', () => {
const startDate = '2/5/2013'; // This date is based on our courseMetadata factory's sample data.
expect(global.location.href).toEqual(`http://localhost/redirect/dashboard?notlive=${startDate}`);
});
it('should go to the enterprise learner dashboard for a course_not_started_enterprise_learner error code', async () => {
setUpWithDeniedStatus('course_not_started_enterprise_learner');
await loadContainer();
expect(global.location.href).toEqual('http://localhost/redirect/enterprise-learner-dashboard');
});
});
});

Some files were not shown because too many files have changed in this diff Show More