feat: lighter build by rewriting lodash imports (#1772)

Incorrect lodash imports are causing MFEs to import the entire lodash
library. This change shaves off a few kB of the compressed build.
This commit is contained in:
Régis Behmo
2025-04-16 02:07:16 +02:00
committed by GitHub
parent f531d5471d
commit 4bd2c3b29a
20 changed files with 126 additions and 118 deletions

View File

@@ -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<Props> = ({ 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,

View File

@@ -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<BlockCardProps> = ({ 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]);

View File

@@ -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 });

View File

@@ -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({

View File

@@ -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)}
>
<div className="x-small">
<FormattedMessage {...messages.unlimitedAttemptsCheckboxLabel} />

View File

@@ -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' };

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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;

View File

@@ -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) && (
<Form.Control.Feedback type="default" key="help-text">
{props.helpText}
</Form.Control.Feedback>
)}
{!_.isEmpty(props.errorMessage) && (
{!isEmpty(props.errorMessage) && (
<Form.Control.Feedback
type="invalid"
key="error"

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect } from 'react';
import _ from 'lodash';
import { isEmpty } from 'lodash';
import { PropTypes } from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
@@ -57,7 +57,7 @@ const TableActions = ({
) : null}
<Dropdown.Item
onClick={() => handleBulkDownload(selectedFlatRows)}
disabled={_.isEmpty(selectedFlatRows)}
disabled={isEmpty(selectedFlatRows)}
>
<FormattedMessage {...messages.downloadTitle} />
</Dropdown.Item>
@@ -65,7 +65,7 @@ const TableActions = ({
<Dropdown.Item
data-testid="open-delete-confirmation-button"
onClick={() => handleOpenDeleteConfirmation(selectedFlatRows)}
disabled={_.isEmpty(selectedFlatRows)}
disabled={isEmpty(selectedFlatRows)}
>
<FormattedMessage {...messages.deleteTitle} />
</Dropdown.Item>

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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]);

View File

@@ -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]);

View File

@@ -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],

View File

@@ -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 = {};