diff --git a/src/course-libraries/CourseLibraries.tsx b/src/course-libraries/CourseLibraries.tsx index fcdea5c98..3c1a0730f 100644 --- a/src/course-libraries/CourseLibraries.tsx +++ b/src/course-libraries/CourseLibraries.tsx @@ -20,7 +20,7 @@ import { Cached, CheckCircle, Launch, Loop, } from '@openedx/paragon/icons'; -import _ from 'lodash'; +import sumBy from 'lodash/sumBy'; import { useSearchParams } from 'react-router-dom'; import getPageHeadTitle from '../generic/utils'; import { useModel } from '../generic/model-store'; @@ -109,7 +109,7 @@ export const CourseLibraries: React.FC = ({ courseId }) => { ); const [showReviewAlert, setShowReviewAlert] = useState(false); const { data: libraries, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId); - const outOfSyncCount = useMemo(() => _.sumBy(libraries, (lib) => lib.readyToSyncCount), [libraries]); + const outOfSyncCount = useMemo(() => sumBy(libraries, (lib) => lib.readyToSyncCount), [libraries]); const { isLoadingPage: isLoadingStudioHome, isFailedLoadingPage: isFailedLoadingStudioHome, diff --git a/src/course-libraries/ReviewTabContent.tsx b/src/course-libraries/ReviewTabContent.tsx index 80a9c12c3..1c756c5ca 100644 --- a/src/course-libraries/ReviewTabContent.tsx +++ b/src/course-libraries/ReviewTabContent.tsx @@ -14,7 +14,9 @@ import { useToggle, } from '@openedx/paragon'; -import _ from 'lodash'; +import { + tail, keyBy, orderBy, merge, omitBy, +} from 'lodash'; import { useQueryClient } from '@tanstack/react-query'; import { Loop, Warning } from '@openedx/paragon/icons'; import messages from './messages'; @@ -49,7 +51,7 @@ interface BlockCardProps { const BlockCard: React.FC = ({ info, actions }) => { const intl = useIntl(); const componentIcon = getItemIcon(info.blockType); - const breadcrumbs = _.tail(info.breadcrumbs) as Array<{ displayName: string, usageKey: string }>; + const breadcrumbs = tail(info.breadcrumbs) as Array<{ displayName: string, usageKey: string }>; const getBlockLink = useCallback(() => { let key = info.usageKey; @@ -138,11 +140,11 @@ const ComponentReviewList = ({ ); const outOfSyncComponentsByKey = useMemo( - () => _.keyBy(outOfSyncComponents, 'downstreamUsageKey'), + () => keyBy(outOfSyncComponents, 'downstreamUsageKey'), [outOfSyncComponents], ); const downstreamInfoByKey = useMemo( - () => _.keyBy(downstreamInfo, 'usageKey'), + () => keyBy(downstreamInfo, 'usageKey'), [downstreamInfo], ); const queryClient = useQueryClient(); @@ -241,9 +243,9 @@ const ComponentReviewList = ({ if (isIndexDataLoading) { return []; } - let merged = _.merge(downstreamInfoByKey, outOfSyncComponentsByKey); - merged = _.omitBy(merged, (o) => !o.displayName); - const ordered = _.orderBy(Object.values(merged), 'updated', 'desc'); + let merged = merge(downstreamInfoByKey, outOfSyncComponentsByKey); + merged = omitBy(merged, (o) => !o.displayName); + const ordered = orderBy(Object.values(merged), 'updated', 'desc'); return ordered; }, [downstreamInfoByKey, outOfSyncComponentsByKey]); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js index 5f10f2289..dffc721af 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; -import _ from 'lodash'; +import { + includes, isEmpty, isFinite, isNaN, isNil, +} from 'lodash'; // This 'module' self-import hack enables mocking during tests. // See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested // should be re-thought and cleaned up to avoid this pattern. @@ -65,7 +67,7 @@ export const hintsCardHooks = (hints, updateSettings) => { const handleAdd = () => { let newId = 0; - if (!_.isEmpty(hints)) { + if (!isEmpty(hints)) { newId = Math.max(...hints.map(hint => hint.id)) + 1; } const hint = { id: newId, value: '' }; @@ -114,9 +116,9 @@ export const resetCardHooks = (updateSettings) => { export const scoringCardHooks = (scoring, updateSettings, defaultValue) => { let loadedAttemptsNumber = scoring.attempts.number; - if ((loadedAttemptsNumber === defaultValue || !_.isFinite(loadedAttemptsNumber)) && _.isFinite(defaultValue)) { + if ((loadedAttemptsNumber === defaultValue || !isFinite(loadedAttemptsNumber)) && isFinite(defaultValue)) { loadedAttemptsNumber = `${defaultValue} (Default)`; - } else if (loadedAttemptsNumber === defaultValue && _.isNil(defaultValue)) { + } else if (loadedAttemptsNumber === defaultValue && isNil(defaultValue)) { loadedAttemptsNumber = ''; } const [attemptDisplayValue, setAttemptDisplayValue] = module.state.attemptDisplayValue(loadedAttemptsNumber); @@ -135,9 +137,9 @@ export const scoringCardHooks = (scoring, updateSettings, defaultValue) => { let unlimitedAttempts = false; let attemptNumber = parseInt(event.target.value, 10); - if (!_.isFinite(attemptNumber) || attemptNumber === defaultValue) { + if (!isFinite(attemptNumber) || attemptNumber === defaultValue) { attemptNumber = null; - if (_.isFinite(defaultValue)) { + if (isFinite(defaultValue)) { setAttemptDisplayValue(`${defaultValue} (Default)`); } else { setAttemptDisplayValue(''); @@ -154,7 +156,7 @@ export const scoringCardHooks = (scoring, updateSettings, defaultValue) => { let newMaxAttempt = parseInt(event.target.value, 10); if (newMaxAttempt === defaultValue) { newMaxAttempt = `${defaultValue} (Default)`; - } else if (_.isNaN(newMaxAttempt)) { + } else if (isNaN(newMaxAttempt)) { newMaxAttempt = ''; } else if (newMaxAttempt < 0) { newMaxAttempt = 0; @@ -164,7 +166,7 @@ export const scoringCardHooks = (scoring, updateSettings, defaultValue) => { const handleWeightChange = (event) => { let weight = parseFloat(event.target.value); - if (_.isNaN(weight) || weight < 0) { + if (isNaN(weight) || weight < 0) { weight = 0; } updateSettings({ scoring: { ...scoring, weight } }); @@ -187,18 +189,18 @@ export const useAnswerSettings = (showAnswer, updateSettings) => { ]; useEffect(() => { - setShowAttempts(_.includes(numberOfAttemptsChoice, showAnswer.on)); + setShowAttempts(includes(numberOfAttemptsChoice, showAnswer.on)); }, [showAttempts]); const handleShowAnswerChange = (event) => { const { value } = event.target; - setShowAttempts(_.includes(numberOfAttemptsChoice, value)); + setShowAttempts(includes(numberOfAttemptsChoice, value)); updateSettings({ showAnswer: { ...showAnswer, on: value } }); }; const handleAttemptsChange = (event) => { let attempts = parseInt(event.target.value, 10); - if (_.isNaN(attempts) || attempts < 0) { + if (isNaN(attempts) || attempts < 0) { attempts = 0; } updateSettings({ showAnswer: { ...showAnswer, afterAttempts: attempts } }); @@ -214,7 +216,7 @@ export const useAnswerSettings = (showAnswer, updateSettings) => { export const timerCardHooks = (updateSettings) => ({ handleChange: (event) => { let time = parseInt(event.target.value, 10); - if (_.isNaN(time) || time < 0) { + if (isNaN(time) || time < 0) { time = 0; } updateSettings({ timeBetween: time }); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js index c168392b3..c8aba6a22 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import _ from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import messages from './messages'; // This 'module' self-import hack enables mocking during tests. // See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested @@ -19,7 +19,7 @@ export const generalFeedbackHooks = (generalFeedback, updateSettings) => { // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { - if (_.isEmpty(generalFeedback)) { + if (isEmpty(generalFeedback)) { setSummary({ message: messages.noGeneralFeedbackSummary, values: {}, intl: true }); } else { setSummary({ diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx index 3935c7cb7..b0b56add1 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ScoringCard.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import _ from 'lodash'; +import isNil from 'lodash/isNil'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -76,7 +76,7 @@ const ScoringCard = ({ className="mt-3 decoration-control-label" checked={scoring.attempts.unlimited} onChange={handleUnlimitedChange} - disabled={!_.isNil(defaultValue)} + disabled={!isNil(defaultValue)} >
diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js index a21d8c05d..4a390e68c 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.js @@ -2,7 +2,9 @@ /* eslint no-eval: 0 */ import { XMLParser, XMLBuilder } from 'fast-xml-parser'; -import _ from 'lodash'; +import { + get, has, keys, isArray, isEmpty, +} from 'lodash'; import { ProblemTypeKeys, RichTextProblems, @@ -92,7 +94,7 @@ export class OLXParser { const parser = new XMLParser(parserOptions); this.builder = new XMLBuilder(builderOptions); this.parsedOLX = parser.parse(olxString); - if (_.has(this.parsedOLX, 'problem')) { + if (has(this.parsedOLX, 'problem')) { this.problem = this.parsedOLX.problem; } @@ -112,7 +114,7 @@ export class OLXParser { const richTextParser = new XMLParser(richTextOptions); this.richTextBuilder = new XMLBuilder(richTextBuilderOptions); this.richTextOLX = richTextParser.parse(olxString); - if (_.has(this.parsedOLX, 'problem')) { + if (has(this.parsedOLX, 'problem')) { this.richTextProblem = this.richTextOLX[0].problem; } } @@ -188,17 +190,17 @@ export class OLXParser { ); const answers = []; let data = {}; - const widget = _.get(this.problem, `${problemType}.${widgetName}`); + const widget = get(this.problem, `${problemType}.${widgetName}`); const permissableTags = ['choice', '@_type', 'compoundhint', 'option', '#text']; - if (_.keys(widget).some((tag) => !permissableTags.includes(tag))) { + if (keys(widget).some((tag) => !permissableTags.includes(tag))) { throw new Error('Misc Tags, reverting to Advanced Editor'); } - if (_.get(this.problem, `${problemType}.@_partial_credit`)) { + if (get(this.problem, `${problemType}.@_partial_credit`)) { throw new Error('Partial credit not supported by GUI, reverting to Advanced Editor'); } - const choice = _.get(widget, option); + const choice = get(widget, option); const isComplexAnswer = RichTextProblems.includes(problemType); - if (_.isEmpty(choice)) { + if (isEmpty(choice)) { answers.push( { id: indexToLetterMap[answers.length], @@ -206,7 +208,7 @@ export class OLXParser { correct: true, }, ); - } else if (_.isArray(choice)) { + } else if (isArray(choice)) { choice.forEach((element, index) => { const preservedAnswer = preservedAnswers[index].filter(answer => !Object.keys(answer).includes(`${option}hint`)); const preservedFeedback = preservedAnswers[index].filter(answer => Object.keys(answer).includes(`${option}hint`)); @@ -265,11 +267,11 @@ export class OLXParser { getAnswerFeedback(preservedFeedback, hintKey) { const feedback = {}; let feedbackKeys = 'selectedFeedback'; - if (_.isEmpty(preservedFeedback)) { return feedback; } + if (isEmpty(preservedFeedback)) { return feedback; } preservedFeedback.forEach((feedbackArr) => { - if (_.has(feedbackArr, hintKey)) { - if (_.has(feedbackArr, ':@') && _.has(feedbackArr[':@'], '@_selected')) { + if (has(feedbackArr, hintKey)) { + if (has(feedbackArr, ':@') && has(feedbackArr[':@'], '@_selected')) { const isSelectedFeedback = feedbackArr[':@']['@_selected'] === 'true'; feedbackKeys = isSelectedFeedback ? 'selectedFeedback' : 'unselectedFeedback'; } @@ -287,9 +289,9 @@ export class OLXParser { */ getGroupedFeedback(choices) { const groupFeedback = []; - if (_.has(choices, 'compoundhint')) { + if (has(choices, 'compoundhint')) { const groupFeedbackArray = choices.compoundhint; - if (_.isArray(groupFeedbackArray)) { + if (isArray(groupFeedbackArray)) { groupFeedbackArray.forEach((element) => { const parsedFeedback = stripNonTextTags({ input: element, tag: '@_value' }); groupFeedback.push({ @@ -338,12 +340,12 @@ export class OLXParser { selectedFeedback: firstFeedback, }); - const additionalAnswerFeedback = preservedFeedback.filter(feedback => _.isArray(feedback)); - const stringEqualHintFeedback = preservedFeedback.filter(feedback => !_.isArray(feedback)); + const additionalAnswerFeedback = preservedFeedback.filter(feedback => isArray(feedback)); + const stringEqualHintFeedback = preservedFeedback.filter(feedback => !isArray(feedback)); // Parsing additional_answer for string response. - const additionalAnswer = _.get(stringresponse, 'additional_answer', []); - if (_.isArray(additionalAnswer)) { + const additionalAnswer = get(stringresponse, 'additional_answer', []); + if (isArray(additionalAnswer)) { additionalAnswer.forEach((newAnswer, indx) => { answerFeedback = this.getFeedback(additionalAnswerFeedback[indx]); answers.push({ @@ -364,8 +366,8 @@ export class OLXParser { } // Parsing stringequalhint for string response. - const stringEqualHint = _.get(stringresponse, 'stringequalhint', []); - if (_.isArray(stringEqualHint)) { + const stringEqualHint = get(stringresponse, 'stringequalhint', []); + if (isArray(stringEqualHint)) { stringEqualHint.forEach((newAnswer, indx) => { answerFeedback = this.getFeedback(stringEqualHintFeedback[indx]?.stringequalhint); answers.push({ @@ -387,9 +389,9 @@ export class OLXParser { // TODO: Support multiple types. additionalStringAttributes = { - type: _.get(stringresponse, '@_type'), + type: get(stringresponse, '@_type'), textline: { - size: _.get(stringresponse, 'textline.@_size'), + size: get(stringresponse, 'textline.@_size'), }, }; @@ -416,16 +418,16 @@ export class OLXParser { 'correcthint', ); const { numericalresponse } = this.problem; - if (_.get(numericalresponse, '@_partial_credit')) { + if (get(numericalresponse, '@_partial_credit')) { throw new Error('Partial credit not supported by GUI, reverting to Advanced Editor'); } let answerFeedback = ''; const answers = []; let responseParam = {}; const feedback = this.getFeedback(firstCorrectFeedback); - if (_.has(numericalresponse, 'responseparam')) { - const type = _.get(numericalresponse, 'responseparam.@_type'); - const defaultValue = _.get(numericalresponse, 'responseparam.@_default'); + if (has(numericalresponse, 'responseparam')) { + const type = get(numericalresponse, 'responseparam.@_type'); + const defaultValue = get(numericalresponse, 'responseparam.@_default'); responseParam = { [type]: defaultValue, }; @@ -441,8 +443,8 @@ export class OLXParser { }); // Parsing additional_answer for numerical response. - const additionalAnswer = _.get(numericalresponse, 'additional_answer', []); - if (_.isArray(additionalAnswer)) { + const additionalAnswer = get(numericalresponse, 'additional_answer', []); + if (isArray(additionalAnswer)) { additionalAnswer.forEach((newAnswer, indx) => { answerFeedback = this.getFeedback(preservedFeedback[indx]); answers.push({ @@ -475,7 +477,7 @@ export class OLXParser { * @return {string} string of OLX */ parseQuestions(problemType) { - const problemArray = _.get(this.richTextProblem[0], problemType) || this.richTextProblem; + const problemArray = get(this.richTextProblem[0], problemType) || this.richTextProblem; const questionArray = []; problemArray.forEach(tag => { @@ -548,7 +550,7 @@ export class OLXParser { */ getHints() { const hintsObject = []; - if (_.has(this.problem, 'demandhint.hint')) { + if (has(this.problem, 'demandhint.hint')) { const preservedProblem = this.richTextProblem; preservedProblem.forEach(obj => { const objKeys = Object.keys(obj); @@ -578,21 +580,21 @@ export class OLXParser { * @return {string} string of OLX */ getSolutionExplanation(problemType) { - if (!_.has(this.problem, `${problemType}.solution`) && !_.has(this.problem, 'solution')) { return null; } + if (!has(this.problem, `${problemType}.solution`) && !has(this.problem, 'solution')) { return null; } const [problemBody] = this.richTextProblem.filter(section => Object.keys(section).includes(problemType)); const [solutionBody] = problemBody[problemType].filter(section => Object.keys(section).includes('solution')); const [divBody] = solutionBody.solution.filter(section => Object.keys(section).includes('div')); const solutionArray = []; if (divBody && divBody.div) { divBody.div.forEach(tag => { - const tagText = _.get(Object.values(tag)[0][0], '#text', ''); + const tagText = get(Object.values(tag)[0][0], '#text', ''); if (tagText.toString().trim() !== 'Explanation') { solutionArray.push(tag); } }); } else { solutionBody.solution.forEach(tag => { - const tagText = _.get(Object.values(tag)[0][0], '#text', ''); + const tagText = get(Object.values(tag)[0][0], '#text', ''); if (tagText.toString().trim() !== 'Explanation') { solutionArray.push(tag); } @@ -610,7 +612,7 @@ export class OLXParser { * @return {string} string of feedback */ getFeedback(xmlElement) { - if (_.isEmpty(xmlElement)) { return ''; } + if (isEmpty(xmlElement)) { return ''; } const feedbackString = this.richTextBuilder.build(xmlElement); return feedbackString; } @@ -636,7 +638,7 @@ export class OLXParser { } // make sure compound problems are treated as advanced if ((problemTypeKeys.length > 1) - || (_.isArray(this.problem[problemTypeKeys[0]]) + || (isArray(this.problem[problemTypeKeys[0]]) && this.problem[problemTypeKeys[0]].length > 1)) { return ProblemTypeKeys.ADVANCED; } @@ -671,7 +673,7 @@ export class OLXParser { } getParsedOLXData() { - if (_.isEmpty(this.problem)) { + if (isEmpty(this.problem)) { return {}; } @@ -720,16 +722,16 @@ export class OLXParser { return {}; } const generalFeedback = this.getGeneralFeedback({ answers: answersObject.answers, problemType }); - if (_.has(answersObject, 'additionalStringAttributes')) { + if (has(answersObject, 'additionalStringAttributes')) { additionalAttributes = { ...answersObject.additionalStringAttributes }; } - if (_.has(answersObject, 'groupFeedbackList')) { + if (has(answersObject, 'groupFeedbackList')) { groupFeedbackList = answersObject.groupFeedbackList; } const { answers } = answersObject; const settings = { hints }; - if (ProblemTypeKeys.NUMERIC === problemType && _.has(answers[0], 'tolerance')) { + if (ProblemTypeKeys.NUMERIC === problemType && has(answers[0], 'tolerance')) { const toleranceValue = answers[0].tolerance; if (!toleranceValue || toleranceValue.length === 0) { settings.tolerance = { value: null, type: 'None' }; diff --git a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js index 673763de6..5f2d8ca9f 100644 --- a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js +++ b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import { get, has } from 'lodash'; import { XMLParser, XMLBuilder } from 'fast-xml-parser'; import { ProblemTypeKeys } from '../../../data/constants/problem'; import { ToleranceTypes } from '../components/EditProblemView/SettingsWidget/settingsComponents/Tolerance/constants'; @@ -169,7 +169,7 @@ class ReactStateOLXParser { choice.push(singleAnswer); } }); - if (_.has(this.problemState, 'groupFeedbackList') && problemType === ProblemTypeKeys.MULTISELECT) { + if (has(this.problemState, 'groupFeedbackList') && problemType === ProblemTypeKeys.MULTISELECT) { compoundhint = this.addGroupFeedbackList(); choice.push(...compoundhint); } @@ -333,7 +333,7 @@ class ReactStateOLXParser { answerObject = { ':@': { '@_answer': answer.title, - '@_type': _.get(this.problemState, 'additionalAttributes.type', 'ci'), + '@_type': get(this.problemState, 'additionalAttributes.type', 'ci'), }, [problemType]: [...correcthint], }; @@ -348,7 +348,7 @@ class ReactStateOLXParser { }); answerObject[problemType].push({ textline: { '#text': '' }, - ':@': { '@_size': _.get(this.problemState, 'additionalAttributes.textline.size', 20) }, + ':@': { '@_size': get(this.problemState, 'additionalAttributes.textline.size', 20) }, }); return answerObject; } @@ -490,7 +490,7 @@ class ReactStateOLXParser { * @return {bool} */ hasAttributeWithValue(obj, attr) { - return _.has(obj, attr) && _.get(obj, attr, '').toString().trim() !== ''; + return has(obj, attr) && get(obj, attr, '').toString().trim() !== ''; } buildOLX() { diff --git a/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js b/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js index 0bc217784..7cad5b16a 100644 --- a/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js +++ b/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js @@ -1,5 +1,5 @@ import { XMLParser } from 'fast-xml-parser'; -import _ from 'lodash'; +import { includes } from 'lodash'; import { ShowAnswerTypesKeys, @@ -34,7 +34,7 @@ class ReactStateSettingsParser { settings = popuplateItem(settings, 'number', 'max_attempts', stateSettings.scoring.attempts, defaultSettings?.maxAttempts, true); settings = popuplateItem(settings, 'weight', 'weight', stateSettings.scoring); settings = popuplateItem(settings, 'on', 'showanswer', stateSettings.showAnswer, defaultSettings?.showanswer, true); - if (_.includes(numberOfAttemptsChoice, stateSettings.showAnswer.on)) { + if (includes(numberOfAttemptsChoice, stateSettings.showAnswer.on)) { settings = popuplateItem(settings, 'afterAttempts', 'attempts_before_showanswer_button', stateSettings.showAnswer); } settings = popuplateItem(settings, 'showResetButton', 'show_reset_button', stateSettings, defaultSettings?.showResetButton, true); diff --git a/src/editors/containers/ProblemEditor/data/SettingsParser.js b/src/editors/containers/ProblemEditor/data/SettingsParser.js index afc56bde4..55a84349f 100644 --- a/src/editors/containers/ProblemEditor/data/SettingsParser.js +++ b/src/editors/containers/ProblemEditor/data/SettingsParser.js @@ -1,15 +1,17 @@ -import _ from 'lodash'; +import { + get, isEmpty, isFinite, isNil, +} from 'lodash'; import { ShowAnswerTypes, RandomizationTypesKeys } from '../../../data/constants/problem'; export const popuplateItem = (parentObject, itemName, statekey, metadata, defaultValue = null, allowNull = false) => { let parent = parentObject; - const item = _.get(metadata, itemName, null); + const item = get(metadata, itemName, null); // if item is null, undefined, or empty string, use defaultValue - const finalValue = (_.isNil(item) || item === '') ? defaultValue : item; + const finalValue = (isNil(item) || item === '') ? defaultValue : item; - if (!_.isNil(finalValue) || allowNull) { + if (!isNil(finalValue) || allowNull) { parent = { ...parentObject, [statekey]: finalValue }; } return parent; @@ -19,18 +21,18 @@ export const parseScoringSettings = (metadata, defaultSettings) => { let scoring = {}; const attempts = popuplateItem({}, 'max_attempts', 'number', metadata); - const initialAttempts = _.get(attempts, 'number', null); - const defaultAttempts = _.get(defaultSettings, 'max_attempts', null); + const initialAttempts = get(attempts, 'number', null); + const defaultAttempts = get(defaultSettings, 'max_attempts', null); attempts.unlimited = false; // isFinite checks if value is a finite primitive number. - if (!_.isFinite(initialAttempts) || initialAttempts === defaultAttempts) { + if (!isFinite(initialAttempts) || initialAttempts === defaultAttempts) { // set number to null in any case as lms will pick default value if it exists. attempts.number = null; } // if both block number and default number are null set unlimited to true. - if (_.isNil(initialAttempts) && _.isNil(defaultAttempts)) { + if (isNil(initialAttempts) && isNil(defaultAttempts)) { attempts.unlimited = true; } @@ -48,8 +50,8 @@ export const parseScoringSettings = (metadata, defaultSettings) => { export const parseShowAnswer = (metadata) => { let showAnswer = {}; - const showAnswerType = _.get(metadata, 'showanswer', {}); - if (!_.isNil(showAnswerType) && showAnswerType in ShowAnswerTypes) { + const showAnswerType = get(metadata, 'showanswer', {}); + if (!isNil(showAnswerType) && showAnswerType in ShowAnswerTypes) { showAnswer = { ...showAnswer, on: showAnswerType }; } @@ -61,22 +63,22 @@ export const parseShowAnswer = (metadata) => { export const parseSettings = (metadata, defaultSettings) => { let settings = {}; - if (_.isNil(metadata) || _.isEmpty(metadata)) { + if (isNil(metadata) || isEmpty(metadata)) { return settings; } const scoring = parseScoringSettings(metadata, defaultSettings); - if (!_.isEmpty(scoring)) { + if (!isEmpty(scoring)) { settings = { ...settings, scoring }; } const showAnswer = parseShowAnswer(metadata); - if (!_.isEmpty(showAnswer)) { + if (!isEmpty(showAnswer)) { settings = { ...settings, showAnswer }; } - const randomizationType = _.get(metadata, 'rerandomize', {}); - if (!_.isEmpty(randomizationType) && Object.values(RandomizationTypesKeys).includes(randomizationType)) { + const randomizationType = get(metadata, 'rerandomize', {}); + if (!isEmpty(randomizationType) && Object.values(RandomizationTypesKeys).includes(randomizationType)) { settings = popuplateItem(settings, 'rerandomize', 'randomization', metadata); } diff --git a/src/editors/data/redux/problem/reducers.ts b/src/editors/data/redux/problem/reducers.ts index f802be028..ed3f91a26 100644 --- a/src/editors/data/redux/problem/reducers.ts +++ b/src/editors/data/redux/problem/reducers.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import { has } from 'lodash'; import { createSlice } from '@reduxjs/toolkit'; import { indexToLetterMap } from '../../../containers/ProblemEditor/data/OLXParser'; import { StrictDict } from '../../../utils'; @@ -61,17 +61,17 @@ const problem = createSlice({ let { correctAnswerCount } = state; const answers = state.answers.map(obj => { if (obj.id === id) { - if (_.has(answer, 'correct') && payload.correct) { + if (has(answer, 'correct') && payload.correct) { correctAnswerCount += 1; } - if (_.has(answer, 'correct') && payload.correct === false && correctAnswerCount > 0) { + if (has(answer, 'correct') && payload.correct === false && correctAnswerCount > 0) { correctAnswerCount -= 1; } return { ...obj, ...answer }; } // set other answers as incorrect if problem only has one answer correct // and changes object include correct key change - if (hasSingleAnswer && _.has(answer, 'correct') && obj.correct) { + if (hasSingleAnswer && has(answer, 'correct') && obj.correct) { return { ...obj, correct: false }; } return obj; diff --git a/src/editors/data/redux/thunkActions/problem.ts b/src/editors/data/redux/thunkActions/problem.ts index 5d4c11df6..c10360e8c 100644 --- a/src/editors/data/redux/thunkActions/problem.ts +++ b/src/editors/data/redux/thunkActions/problem.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import { get, isEmpty } from 'lodash'; import { actions as problemActions } from '../problem'; import { actions as requestActions } from '../requests'; import { selectors as appSelectors } from '../app'; @@ -47,7 +47,7 @@ export const getDataFromOlx = ({ rawOLX, rawSettings, defaultSettings }) => { } const { settings, ...data } = parsedProblem; const parsedSettings = { ...settings, ...parseSettings(rawSettings, defaultSettings) }; - if (!_.isEmpty(rawOLX) && !_.isEmpty(data)) { + if (!isEmpty(rawOLX) && !isEmpty(data)) { return { ...data, rawOLX, settings: parsedSettings }; } return { settings: parsedSettings }; @@ -79,8 +79,8 @@ export const fetchAdvancedSettings = ({ rawOLX, rawSettings }) => (dispatch) => }; export const initializeProblem = (blockValue) => (dispatch, getState) => { - const rawOLX = _.get(blockValue, 'data.data', ''); - const rawSettings = _.get(blockValue, 'data.metadata', {}); + const rawOLX = get(blockValue, 'data.data', ''); + const rawSettings = get(blockValue, 'data.metadata', {}); const learningContextId = selectors.app.learningContextId(getState()); if (isLibraryKey(learningContextId)) { // Content libraries don't yet support defaults for fields like max_attempts, showanswer, etc. diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index c84a3913c..1bb2a8db6 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -1,4 +1,4 @@ -import _, { isEmpty } from 'lodash'; +import { has, find, isEmpty } from 'lodash'; import { removeItemOnce } from '../../../utils'; import * as requests from './requests'; // This 'module' self-import hack enables mocking during tests. @@ -21,8 +21,8 @@ export const loadVideoData = (selectedVideoId, selectedVideoUrl) => (dispatch, g let rawVideoData = blockValueData?.metadata ? blockValueData.metadata : {}; const rawVideos = Object.values(selectors.app.videos(state)); if (selectedVideoId !== undefined && selectedVideoId !== null) { - const selectedVideo = _.find(rawVideos, video => { - if (_.has(video, 'edx_video_id')) { + const selectedVideo = find(rawVideos, video => { + if (has(video, 'edx_video_id')) { return video.edx_video_id === selectedVideoId; } return false; diff --git a/src/editors/sharedComponents/TypeaheadDropdown/FormGroup.jsx b/src/editors/sharedComponents/TypeaheadDropdown/FormGroup.jsx index f0739e1e4..683ae2107 100644 --- a/src/editors/sharedComponents/TypeaheadDropdown/FormGroup.jsx +++ b/src/editors/sharedComponents/TypeaheadDropdown/FormGroup.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import _ from 'lodash'; +import { isEmpty } from 'lodash'; import { Form } from '@openedx/paragon'; const FormGroup = (props) => { @@ -35,13 +35,13 @@ const FormGroup = (props) => { {props.children} - {props.helpText && _.isEmpty(props.errorMessage) && ( + {props.helpText && isEmpty(props.errorMessage) && ( {props.helpText} )} - {!_.isEmpty(props.errorMessage) && ( + {!isEmpty(props.errorMessage) && ( handleBulkDownload(selectedFlatRows)} - disabled={_.isEmpty(selectedFlatRows)} + disabled={isEmpty(selectedFlatRows)} > @@ -65,7 +65,7 @@ const TableActions = ({ handleOpenDeleteConfirmation(selectedFlatRows)} - disabled={_.isEmpty(selectedFlatRows)} + disabled={isEmpty(selectedFlatRows)} > diff --git a/src/generic/course-upload-image/index.jsx b/src/generic/course-upload-image/index.jsx index 42a4bb572..d0cff97fb 100644 --- a/src/generic/course-upload-image/index.jsx +++ b/src/generic/course-upload-image/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import _ from 'lodash'; +import last from 'lodash/last'; import { useParams } from 'react-router-dom'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { FileUpload as FileUploadIcon } from '@openedx/paragon/icons'; @@ -35,9 +35,9 @@ const CourseUploadImage = ({ const assetsUrl = () => new URL(`/assets/${courseId}`, getConfig().STUDIO_BASE_URL); const handleChangeImageAsset = (path) => { - const assetPath = _.last(path.split('/')); + const assetPath = last(path.split('/')); // If image path is entered directly, we need to strip the asset prefix - const imageName = _.last(assetPath.split('block@')); + const imageName = last(assetPath.split('block@')); onChange(path, assetImageField); if (imageNameField) { onChange(imageName, imageNameField); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx index ebc6663f9..ea1b18028 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx @@ -5,7 +5,7 @@ import { } from '@openedx/paragon'; import { AppContext } from '@edx/frontend-platform/react'; import { FieldArray, useFormikContext } from 'formik'; -import _ from 'lodash'; +import size from 'lodash/size'; import { useParams } from 'react-router-dom'; import FormSwitchGroup from '../../../../../generic/FormSwitchGroup'; import messages from '../../messages'; @@ -34,7 +34,7 @@ const DivisionByGroupFields = ({ intl }) => { useEffect(() => { if (divideByCohorts) { - if (!divideCourseTopicsByCohorts && _.size(discussionTopics) !== _.size(divideDiscussionIds)) { + if (!divideCourseTopicsByCohorts && size(discussionTopics) !== size(divideDiscussionIds)) { setFieldValue('divideDiscussionIds', discussionTopics.map(topic => topic.id)); } } else { diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionItem.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionItem.jsx index 8bf79dc97..f9010a345 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionItem.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionItem.jsx @@ -3,7 +3,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Form } from '@openedx/paragon'; import { useFormikContext } from 'formik'; import PropTypes from 'prop-types'; -import _ from 'lodash'; +import { startCase, toLower } from 'lodash'; import messages from '../../../messages'; import RestrictDatesInput from './RestrictDatesInput'; @@ -53,7 +53,7 @@ const DiscussionRestrictionItem = ({ collapseHeadingText={formatRestrictedDates(restrictedDate)} badgeVariant={badgeVariant[restrictedDate.status]} badgeStatus={intl.formatMessage(messages.restrictedDatesStatus, { - status: _.startCase(_.toLower(restrictedDate.status)), + status: startCase(toLower(restrictedDate.status)), })} /> ), [restrictedDate]); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx index b0a4959f9..d279533f5 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx @@ -4,7 +4,7 @@ import { Button } from '@openedx/paragon'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FieldArray, useFormikContext } from 'formik'; import { v4 as uuid } from 'uuid'; -import _ from 'lodash'; +import { remove as removeElements, uniq, uniqBy } from 'lodash'; import messages from '../../../messages'; import TopicItem from './TopicItem'; import { OpenedXConfigFormContext } from '../../openedx/OpenedXConfigFormProvider'; @@ -36,10 +36,10 @@ const DiscussionTopics = ({ intl }) => { } else { setValidDiscussionTopics(currentValidTopics => { const allDiscussionTopics = [...currentValidTopics, ...discussionTopics.filter(topic => topic.id === id)]; - const allValidTopics = _.remove(allDiscussionTopics, topic => topic.name !== ''); - return _.uniqBy(allValidTopics, 'id'); + const allValidTopics = removeElements(allDiscussionTopics, topic => topic.name !== ''); + return uniqBy(allValidTopics, 'id'); }); - setFieldValue('divideDiscussionIds', _.uniq([...divideDiscussionIds, id])); + setFieldValue('divideDiscussionIds', uniq([...divideDiscussionIds, id])); } }, [divideDiscussionIds, discussionTopics]); diff --git a/src/pages-and-resources/discussions/app-config-form/utils.js b/src/pages-and-resources/discussions/app-config-form/utils.js index 0584c940b..90a71d58d 100644 --- a/src/pages-and-resources/discussions/app-config-form/utils.js +++ b/src/pages-and-resources/discussions/app-config-form/utils.js @@ -1,5 +1,5 @@ import moment from 'moment'; -import _ from 'lodash'; +import orderBy from 'lodash/orderBy'; import { getIn } from 'formik'; import { restrictedDatesStatus as constants } from '../data/constants'; @@ -48,7 +48,7 @@ export const decodeDateTime = (date, time) => { }; export const sortRestrictedDatesByStatus = (data, status, order) => ( - _.orderBy( + orderBy( data.filter(date => date.status === status), [(obj) => decodeDateTime(obj.startDate, startOfDayTime(obj.startTime))], [order], diff --git a/src/pages-and-resources/discussions/app-list/FeaturesTable.jsx b/src/pages-and-resources/discussions/app-list/FeaturesTable.jsx index 011f2bbc8..b3772ad31 100644 --- a/src/pages-and-resources/discussions/app-list/FeaturesTable.jsx +++ b/src/pages-and-resources/discussions/app-list/FeaturesTable.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Remove, Check } from '@openedx/paragon/icons'; import { DataTable } from '@openedx/paragon'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import _ from 'lodash'; +import groupBy from 'lodash/groupBy'; import messages from './messages'; import appMessages from '../app-config-form/messages'; @@ -13,7 +13,7 @@ import './FeaturesTable.scss'; const FeaturesTable = ({ apps, features, intl }) => { const { basic, partial, full, common, - } = _.groupBy(features, (feature) => feature.featureSupportType); + } = groupBy(features, (feature) => feature.featureSupportType); const createRow = (feature) => { const appCheckmarkCells = {};