diff --git a/.circleci/config.yml b/.circleci/config.yml index becf059760f..c81f0da05bc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,8 +55,8 @@ commands: export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" - nvm install v14.16.1 - nvm alias default 14.16.1 + nvm install v14.18.2 + nvm alias default 14.18.2 echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV @@ -64,9 +64,9 @@ commands: # Restore cached Node dependencies - restore_cache: keys: - - v14.16.1-node-{{ checksum "client/yarn.lock" }} + - v14.18.2-node-{{ checksum "client/yarn.lock" }} # Fallback to using the latest cache if no exact match is found - - v14.16.1-node- + - v14.18.2-node- # Update Node dependencies - run: cd client && yarn install && cd - @@ -75,7 +75,7 @@ commands: - save_cache: paths: - ./client/node_modules - key: v14.16.1-node-{{ checksum "client/yarn.lock" }} + key: v14.18.2-node-{{ checksum "client/yarn.lock" }} build_client: steps: @@ -136,7 +136,7 @@ jobs: command: | bundle exec rake db:setup environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + DATABASE_URL: 'postgres://ubuntu@localhost:5432/coursemology_test' # Run tests! - run: @@ -154,7 +154,7 @@ jobs: --format progress \ $TEST_FILES environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + DATABASE_URL: 'postgres://ubuntu@localhost:5432/coursemology_test' # Collect reports - store_test_results: @@ -239,7 +239,7 @@ jobs: command: | bundle exec rake db:setup environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + DATABASE_URL: 'postgres://ubuntu@localhost:5432/coursemology_test' # Run i18n checks! - run: @@ -249,7 +249,7 @@ jobs: bundle exec i18n-tasks missing bundle exec rake factory_bot:lint environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + DATABASE_URL: 'postgres://ubuntu@localhost:5432/coursemology_test' workflows: version: 2 diff --git a/.gitignore b/.gitignore index be99088e094..336cf6e8a80 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ # Ignore installed node libraries and log npm-debug.log* +yarn-debug.log* +yarn-error.log* node_modules # Ignore generated js bundles diff --git a/Gemfile b/Gemfile index a04af9f9fe8..6b6832500cb 100644 --- a/Gemfile +++ b/Gemfile @@ -213,11 +213,12 @@ gem 'simple_form' gem 'simple_form-bootstrap', git: 'https://github.com/raymondtangsc/simple_form-bootstrap' # Dynamic nested forms gem 'cocoon' +gem 'momentjs-rails' # Needed for bootstrap3-datetimepicker-rails gem 'bootstrap3-datetimepicker-rails' gem 'bootstrap-select-rails' gem 'bootstrap_tokenfield_rails' gem 'twitter-typeahead-rails' -gem 'summernote-rails' +gem 'summernote-rails', git: 'https://github.com/noesya/summernote-rails' # Using CarrierWave for file uploads gem 'carrierwave' diff --git a/Gemfile.lock b/Gemfile.lock index 4ffd021dac4..64be9d7b5fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,6 +43,13 @@ GIT specs: rwordnet (2.0.0) +GIT + remote: https://github.com/noesya/summernote-rails + revision: 6dcff6c2a0e56492dbbcaf0a91ccc5898e6be922 + specs: + summernote-rails (0.8.20.1) + railties (>= 3.1) + GIT remote: https://github.com/raymondtangsc/rails_utils.git revision: 5c8c0caacf08985ae14c5d007529a09d5f284da0 @@ -365,7 +372,7 @@ GEM rake mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.6.1) + mini_portile2 (2.8.0) minitest (5.15.0) momentjs-rails (2.20.1) railties (>= 3.1) @@ -373,8 +380,8 @@ GEM mustermann (1.1.1) ruby2_keywords (~> 0.0.1) nio4r (2.5.8) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + nokogiri (1.13.3) + mini_portile2 (~> 2.8.0) racc (~> 1.4) orm_adapter (0.5.0) parallel (1.21.0) @@ -580,8 +587,6 @@ GEM sprockets (>= 3.0.0) ssrf_filter (1.0.7) stackprof (0.2.19) - summernote-rails (0.8.12.0) - railties (>= 3.1) temple (0.8.2) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -691,6 +696,7 @@ DEPENDENCIES loofah (>= 2.2.1) mimemagic (= 0.3.10) mini_magick + momentjs-rails nokogiri (>= 1.8.1) parallel_tests pg @@ -729,7 +735,7 @@ DEPENDENCIES spring sprockets (< 4.0.0) stackprof - summernote-rails + summernote-rails! themes_on_rails (>= 0.3.1)! traceroute twitter-typeahead-rails @@ -745,4 +751,4 @@ DEPENDENCIES yard BUNDLED WITH - 2.2.22 + 2.2.32 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 17a8259e88b..3a37974ef6f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,6 +14,7 @@ //= require jquery_ujs //= require i18n/translations //= require js-routes +//= require moment //= require bootstrap-sprockets //= require bootstrap-datetimepicker //= require bootstrap-select diff --git a/app/assets/javascripts/course/discussion/topics.js b/app/assets/javascripts/course/discussion/topics.js index 944db373dda..f1e34eabfac 100644 --- a/app/assets/javascripts/course/discussion/topics.js +++ b/app/assets/javascripts/course/discussion/topics.js @@ -29,7 +29,7 @@ // TODO: Display error messages. } - $(document).ready(function () { + $(function () { showCommentBoxes(); DISCUSSION_POST_HELPERS.initializeToolbar(document, DOCUMENT_SELECTOR); }); diff --git a/app/assets/javascripts/helpers/discussion/post_helpers.js b/app/assets/javascripts/helpers/discussion/post_helpers.js index ad953c2c87e..c0939d8c3d3 100644 --- a/app/assets/javascripts/helpers/discussion/post_helpers.js +++ b/app/assets/javascripts/helpers/discussion/post_helpers.js @@ -31,7 +31,7 @@ var DISCUSSION_POST_HELPERS = (function ( * @param {String} selector The selector for the specific discussion posts. */ function initializeToolbar(element, selector) { - $(document).ready(function () { + $(function () { showCommentToolbar(element, selector); }); EVENT_HELPERS.onNodesInserted($(element), function (insertedElement) { diff --git a/app/assets/javascripts/layout.js b/app/assets/javascripts/layout.js index 724fde9fc07..ba6c63a301a 100644 --- a/app/assets/javascripts/layout.js +++ b/app/assets/javascripts/layout.js @@ -163,7 +163,7 @@ // // This prevents missing definitions for things like Ace themes, which are loaded after the // application script. - $(document).ready(function () { + $(function () { initializeComponents(document); EVENT_HELPERS.onNodesInserted($(document), initializeComponents); diff --git a/app/assets/javascripts/patches/dropdown_mobile_fix.js b/app/assets/javascripts/patches/dropdown_mobile_fix.js index 7fde8c0cda0..9ccd566d0af 100644 --- a/app/assets/javascripts/patches/dropdown_mobile_fix.js +++ b/app/assets/javascripts/patches/dropdown_mobile_fix.js @@ -11,7 +11,7 @@ }); } - $(document).ready(function () { + $(function () { initializeDropdownEventListener(); }); })(jQuery); diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 54c82c9dcac..c9ae436c431 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -31,6 +31,12 @@ textarea.code { .note-editor { @extend .note-editor, .note-frame; @extend .panel; + + .note-popover .popover-content { + line-height: 2.5; + padding: 5px; + padding-right: 0; + } } table.codehilite { @@ -76,3 +82,20 @@ table.codehilite { .attachment-uploader { margin-bottom: 1em; } + +// Summernote defaults +$summernote-border-color: #e4e4e4; +$summernote-font-color: #000000; + +.note-editor, +.note-editor.note-airframe { + border: 1px solid $summernote-border-color; + overflow: visible; +} + +.note-editor.note-airframe .note-editing-area .note-editable, +.note-editor.note-frame .note-editing-area .note-editable, +.note-editor .note-editing-area .note-editable { + color: $summernote-font-color; + padding: 10px; +} diff --git a/client/.babelrc b/client/.babelrc index deb70ac63a0..5bbdc54c1f1 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -1,7 +1,12 @@ { "presets": [ "@babel/preset-env", - "@babel/preset-react", + [ + "@babel/preset-react", + { + "runtime": "automatic" + } + ] ], "plugins": [ "@babel/plugin-syntax-dynamic-import", diff --git a/client/.eslintrc.js b/client/.eslintrc.js index b6e1018b8b5..ed0acf05264 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,19 +1,28 @@ module.exports = { parser: 'babel-eslint', + plugins: ['jest'], extends: [ 'airbnb', 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:import/recommended', 'prettier', ], settings: { 'import/resolver': { - node: {}, - webpack: {}, + alias: { + map: [ + ['lib', './app/lib'], + ['api', './app/api'], + ['course', './app/bundles/course'], + ['testUtils', './app/__test__/utils'], + ], + extensions: ['.js', '.jsx'], + }, }, react: { - // TODO: Update this to 'detect' once React is upgraded >=16.9 - version: '16.8.6', + version: 'detect', }, }, rules: { @@ -34,7 +43,7 @@ module.exports = { 'jsx-a11y/mouse-events-have-key-events': 'off', 'jsx-a11y/no-static-element-interactions': 'off', 'max-len': ['warn', 120], - camelcase: ['warn', { properties: 'never' }], + camelcase: ['warn', { properties: 'never', allow: ['^UNSAFE_'] }], 'comma-dangle': ['error', 'always-multiline'], 'func-names': 'off', 'no-multi-str': 'off', @@ -55,4 +64,54 @@ module.exports = { File: true, FileReader: true, }, + overrides: [ + { + files: [ + '**/__test__/**/*.js', + '**/__test__/**/*.jsx', + '**/*.test.js', + '**/*.test.jsx', + '**/*.spec.js', + '**/*.spec.jsx', + ], + env: { + jest: true, + }, + globals: { + courseId: true, + intl: true, + intlShape: true, + sleep: true, + buildContextOptions: true, + localStorage: true, + }, + rules: { + 'jest/no-disabled-tests': 'error', + 'jest/no-focused-tests': 'error', + 'jest/no-alias-methods': 'error', + 'jest/no-identical-title': 'error', + 'jest/no-jasmine-globals': 'error', + 'jest/no-jest-import': 'error', + 'jest/no-test-prefixes': 'error', + 'jest/no-done-callback': 'error', + 'jest/no-test-return-statement': 'error', + 'jest/prefer-to-be': 'error', + 'jest/prefer-to-contain': 'error', + 'jest/prefer-to-have-length': 'error', + 'jest/prefer-spy-on': 'error', + 'jest/valid-expect': 'error', + 'jest/no-deprecated-functions': 'error', + 'react/no-find-dom-node': 'off', + 'react/jsx-filename-extension': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/extensions': 'off', + 'import/no-unresolved': [ + 'error', + { + ignore: ['utils/'], + }, + ], + }, + }, + ], }; diff --git a/client/.eslintrc.test b/client/.eslintrc.test deleted file mode 100644 index 18755ffda6a..00000000000 --- a/client/.eslintrc.test +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": ".eslintrc.js", - "env": { - "jest": true - }, - "globals": { - "courseId": true, - "intl": true, - "intlShape": true, - "sleep": true, - "buildContextOptions": true, - "localStorage": true, - }, - "rules": { - "react/no-find-dom-node": "off", - "react/jsx-filename-extension": "off", - "import/no-extraneous-dependencies": "off", - "import/extensions": "off", - "import/no-unresolved": [ - "error", - { - "ignore": [ 'utils/' ] - } - ] - }, -} diff --git a/client/.prettierrc.js b/client/.prettierrc.js index 2ccf5c4bd93..b09b48c1e77 100644 --- a/client/.prettierrc.js +++ b/client/.prettierrc.js @@ -1,6 +1,11 @@ module.exports = { - semi: true, - trailingComma: 'all', arrowParens: 'always', + endOfLine: 'lf', + jsxSingleQuote: false, + quoteProps: 'as-needed', + semi: true, singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: false, }; diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js index a4761af1e6b..087bd27781e 100644 --- a/client/app/__test__/setup.js +++ b/client/app/__test__/setup.js @@ -3,7 +3,7 @@ import { IntlProvider, intlShape } from 'react-intl'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; +import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; Enzyme.configure({ adapter: new Adapter() }); @@ -16,6 +16,7 @@ const intlProvider = new IntlProvider({ locale: 'en', timeZone }, {}); const courseId = '1'; const muiTheme = getMuiTheme(); +const intl = intlProvider.getChildContext().intl; const buildContextOptions = (store) => ({ context: { intl, store, muiTheme }, childContextTypes: { @@ -28,7 +29,7 @@ const buildContextOptions = (store) => ({ // Global variables global.courseId = courseId; global.window = window; -global.intl = intlProvider.getChildContext().intl; +global.intl = intl; global.intlShape = intlShape; global.muiTheme = muiTheme; global.$ = jQuery; @@ -50,8 +51,6 @@ global.sleep = sleep; // summernote does not work well with jsdom in tests, stub it to normal text field. jest.mock('lib/components/redux-form/RichTextField', () => { - const TextField = require.requireActual( - 'lib/components/redux-form/TextField', - ); + const TextField = jest.requireActual('lib/components/redux-form/TextField'); return TextField; }); diff --git a/client/app/__test__/utils/__test__/shallowUntil.test.js b/client/app/__test__/utils/__test__/shallowUntil.test.js index b9d4fffd94e..0da0448b436 100644 --- a/client/app/__test__/utils/__test__/shallowUntil.test.js +++ b/client/app/__test__/utils/__test__/shallowUntil.test.js @@ -1,11 +1,11 @@ -import React from 'react'; +import { Component } from 'react'; import PropTypes from 'prop-types'; import shallowUntil from '../shallowUntil'; describe('#shallowUntil', () => { const Div = () =>
; - const hoc = (Component) => { - const component = () =>EXPECT_*
,
+ googletest: (
+
+ {intl.formatMessage(
+ translations.testCaseDescriptionGoogleTest,
+ )}
+
+ ),
+ tostring: (
+
+
+ std::to_string
+
+
+ ),
+ }}
+ />
+ EXPECT_*
,
- googletest: (
-
- {intl.formatMessage(
- translations.testCaseDescriptionGoogleTest,
- )}
-
- ),
- tostring: (
-
-
- std::to_string
-
-
- ),
- }}
- />
- {intl.formatMessage(javaTranslations.expectEquals)}
+ ),
+ }}
+ />
+ ++ ), + /* eslint-enable react/jsx-indent */ + codeExampleExpected:+ {'int array [] = {0,0,0}; // Initialize variables'} +
++ addOneToArray(array); // Make function calls +
++ int expected [] ={'{'} + 1,1,1 + {'}'}; // Make function calls +
++ setAttribute("expression", + "addOneToArray([0,0,0])"); + {' // Override the default expression displayed'} +
+
expected
,
+ codeExampleExpression: array
,
+ }}
+ />
+ {intl.formatMessage(javaTranslations.expectEquals)}
- ),
- }}
- />
- -- ), - /* eslint-enable react/jsx-indent */ - codeExampleExpected:- {'int array [] = {0,0,0}; // Initialize variables'} -
-- addOneToArray(array); // Make function calls -
-- int expected [] ={'{'} - 1,1,1 - {'}'}; // Make function calls -
-- {'setAttribute("expression", "addOneToArray([0,0,0])");'} - {' // Override the default expression displayed'} -
-
expected
,
- codeExampleExpression: array
,
- }}
- />
-
+ {intl.formatMessage(translations.testCaseDescriptionPrint)}
+
+ ),
+ none: (
+
+ {intl.formatMessage(translations.testCaseDescriptionNone)}
+
+ ),
+ }}
+ />
+
- {intl.formatMessage(translations.testCaseDescriptionPrint)}
-
- ),
- none: (
-
- {intl.formatMessage(translations.testCaseDescriptionNone)}
-
- ),
- }}
- />
-
+ return (
+ <>
+ toggleComment(lineNumber)}
+ onMouseOver={() => setLineHovered(lineNumber)}
+ onMouseOut={() => setLineHovered(0)}
+ ref={containerRef}
+ >
+
+ {renderComments(lineNumber)}
+ >
+ );
+};
+
+LineNumberColumn.propTypes = {
+ lineNumber: PropTypes.number.isRequired,
+ lineHovered: PropTypes.number.isRequired,
+ setLineHovered: PropTypes.func.isRequired,
+ setActiveComment: PropTypes.func.isRequired,
+ toggleComment: PropTypes.func.isRequired,
+ expandComment: PropTypes.func.isRequired,
+ activeComment: PropTypes.number.isRequired,
+ editorWidth: PropTypes.number.isRequired,
+
+ expanded: PropTypes.arrayOf(PropTypes.bool).isRequired,
+ answerId: PropTypes.number.isRequired,
+ fileId: PropTypes.number.isRequired,
+ annotations: PropTypes.arrayOf(annotationShape),
+ collapseLine: PropTypes.func,
+};
+
+export default function NarrowEditor(props) {
+ const editorRef = useRef();
+ const [editorWidth, setEditorWidth] = useState(0);
+ const [activeComment, setActiveComment] = useState(0);
+ const [lineHovered, setLineHovered] = useState(0);
+
+ const getEditorWidth = useCallback(() => {
+ if (!editorRef || !editorRef.current) {
+ return;
+ }
+ setEditorWidth(editorRef.current.clientWidth - 50); // 50 is the width of the line number column
+ }, [editorRef]);
+
+ useEffect(() => {
+ getEditorWidth();
+ }, [getEditorWidth]);
+
+ useEffect(() => {
+ window.addEventListener('resize', getEditorWidth);
+
+ return () => window.removeEventListener('resize', getEditorWidth);
+ }, [getEditorWidth]);
+
+ const expandComment = (lineNumber) => {
+ props.expandLine(lineNumber);
+ setActiveComment(lineNumber);
+ };
+
+ const toggleComment = (lineNumber) => {
+ props.toggleLine(lineNumber);
+ setActiveComment(lineNumber);
+ };
+
+ const renderLineNumberColumn = (lineNumber) => (
+ {lineNumber}
+
|
+
{output}+
{output}-
+
-
- {intl.formatMessage(questionFormTranslations.gridViewHint)} -
- > ++ {intl.formatMessage(questionFormTranslations.gridViewHint)} +
+ > ); } - renderMultipleResponseFields() { - const { intl } = this.props; + renderValidOptionCount() { + const { intl, formValues } = this.props; + const numberOfFilledOptions = formValues + ? // eslint-disable-next-line react/prop-types + countFilledOptions(formValues.options) + : 0; + return ( -foo bar baz
Link to bar'; const str2 = 'hello