From 26eafdc341867fb4f5ac8634c0abab67f2688696 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:53:23 +0800 Subject: [PATCH 1/4] fix(locales): invalid interpolation patterns --- client/locales/zh.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/locales/zh.json b/client/locales/zh.json index df0f47c1a4d..80a5747cf71 100644 --- a/client/locales/zh.json +++ b/client/locales/zh.json @@ -3312,7 +3312,7 @@ "defaultMessage": "运行代码" }, "course.assessment.submission.runCodeWithLimit": { - "defaultMessage": "运行代码({attempts Left, pleural, one {# attempt} other {# attempts}} left)" + "defaultMessage": "运行代码({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" }, "course.assessment.submission.saveDraft": { "defaultMessage": "保存草稿" @@ -4725,7 +4725,7 @@ "defaultMessage": "无法创建 {numFailed} 个组。" }, "course.group.GroupShow.GroupManager.GroupManager.createMultiplePartialFailure": { - "defaultMessage": "无法创建 {numFailed} {numFailed,复数,一个 {group} 其他 {groups}}。" + "defaultMessage": "无法创建 {numFailed} {numFailed, plural, one {group} other {groups}}。" }, "course.group.GroupShow.GroupManager.GroupManager.createMultipleSuccess": { "defaultMessage": "{numCreated} {numCreated, plural, one {group was} other {groups were}} 已成功创建。" From 833273ce08b11e71f5c779b8aee1e1c31b5eb736 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:53:56 +0800 Subject: [PATCH 2/4] fix(AchievementReordering): clarify button label --- .../achievement/components/misc/AchievementReordering.tsx | 2 +- client/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx index e0011aa1349..e1bdb3dee55 100644 --- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx +++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx @@ -26,7 +26,7 @@ const translations = defineMessages({ }, endReorderAchievement: { id: 'course.achievement.AchievementReordering.endReorderAchievement', - defaultMessage: 'Save New Ordering', + defaultMessage: 'Done reordering', }, updateFailed: { id: 'course.achievement.AchievementReordering.updateFailed', diff --git a/client/locales/en.json b/client/locales/en.json index deac903fe32..8bc2de49963 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -306,7 +306,7 @@ "defaultMessage": "New Achievement" }, "course.achievement.AchievementReordering.endReorderAchievement": { - "defaultMessage": "Save New Ordering" + "defaultMessage": "Done reordering" }, "course.achievement.AchievementReordering.startReorderAchievement": { "defaultMessage": "Reorder" From d6aef7e6450d99b560c8b7d73f0006050fe45e28 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:54:38 +0800 Subject: [PATCH 3/4] perf(jquery): lazy load jquery and ace language modes - remove jQuery check from `wait_for_ajax` rspec helper - lazy load jQuery, jQuery UI for achievement reordering page - lazy load non-python language modes and removed jquery initializer --- client/app/__test__/setup.js | 4 - .../components/misc/AchievementReordering.tsx | 97 ++++++++----- .../LiveFeedbackHistory/LiveFeedbackFiles.tsx | 5 +- .../components/answers/Programming/index.jsx | 3 - client/app/initializers.js | 1 - .../components/core/fields/EditorField.tsx | 52 ++++++- .../lib/components/wrappers/ThemeProvider.tsx | 2 +- client/app/lib/initializers/ace-editor.js | 129 ------------------ client/webpack.common.js | 7 - spec/support/capybara.rb | 2 +- 10 files changed, 116 insertions(+), 186 deletions(-) delete mode 100644 client/app/lib/initializers/ace-editor.js diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js index f34b92f8144..0fcb817bfdd 100644 --- a/client/app/__test__/setup.js +++ b/client/app/__test__/setup.js @@ -15,8 +15,6 @@ import './mocks/matchMedia'; Enzyme.configure({ adapter: new Adapter() }); require('@babel/polyfill'); -// Our jquery is from CDN and loaded at runtime, so this is required in test. -const jQuery = require('jquery'); const timeZone = 'Asia/Singapore'; const intlCache = createIntlCache(); @@ -47,8 +45,6 @@ const buildContextOptions = (store) => { global.courseId = courseId; global.window = window; global.muiTheme = muiTheme; -global.$ = jQuery; -global.jQuery = jQuery; global.buildContextOptions = buildContextOptions; window.history.pushState({}, '', `/courses/${courseId}`); diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx index e1bdb3dee55..89288695de2 100644 --- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx +++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx @@ -1,24 +1,16 @@ +import { useRef, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { Button } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; import CourseAPI from 'api/course'; import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; -require('jquery-ui/ui/widgets/sortable'); - interface AchievementReorderingProps { handleReordering: (state: boolean) => void; isReordering: boolean; } -const styles = { - AchievementReorderingButton: { - fontSize: 14, - marginRight: 12, - }, -}; - const translations = defineMessages({ startReorderAchievement: { id: 'course.achievement.AchievementReordering.startReorderAchievement', @@ -38,12 +30,6 @@ const translations = defineMessages({ }, }); -// Serialise the ordered achievements as data for the API call. -function serializedOrdering(): string { - const options = { attribute: 'achievementid', key: 'achievement_order[]' }; - return $('tbody').first().sortable('serialize', options); -} - const AchievementReordering = ( props: AchievementReorderingProps, ): JSX.Element => { @@ -60,35 +46,80 @@ const AchievementReordering = ( } } + const [loadingSortable, setLoadingSortable] = useState(false); + + const sortableCallbacksRef = useRef<{ + enable: () => void; + disable: () => void; + }>(); + return ( - + ); }; diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx index 192f1e1c003..f605762135f 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackFiles.tsx @@ -1,5 +1,4 @@ -import { FC, useRef, useState } from 'react'; -import ReactAce from 'react-ace'; +import { ComponentRef, FC, useRef, useState } from 'react'; import { MessageFile } from 'types/course/assessment/submission/liveFeedback'; import ProgrammingFileDownloadChip from 'course/assessment/submission/components/answers/Programming/ProgrammingFileDownloadChip'; @@ -12,7 +11,7 @@ interface Props { const LiveFeedbackFiles: FC = (props) => { const { file } = props; - const editorRef = useRef(null); + const editorRef = useRef>(null); const [selectedLine, setSelectedLine] = useState(1); diff --git a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx index 681af3e71ba..27d3d871e3b 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx @@ -7,9 +7,6 @@ import { getIsSavingAnswer } from 'course/assessment/submission/selectors/answer import { getSubmission } from 'course/assessment/submission/selectors/submissions'; import { useAppSelector } from 'lib/hooks/store'; -import 'ace-builds/src-noconflict/mode-python'; -import 'ace-builds/src-noconflict/theme-github'; - import CodaveriFeedbackStatus from '../../../containers/CodaveriFeedbackStatus'; import ProgrammingImportEditor from '../../../containers/ProgrammingImport/ProgrammingImportEditor'; import { questionShape } from '../../../propTypes'; diff --git a/client/app/initializers.js b/client/app/initializers.js index eb9d60a4816..264d98d39d0 100644 --- a/client/app/initializers.js +++ b/client/app/initializers.js @@ -1,7 +1,6 @@ /* eslint-disable global-require */ function loadModules() { - require('lib/initializers/ace-editor'); // Require web font last so that it doesn't block the load of current module. require('lib/initializers/webfont'); } diff --git a/client/app/lib/components/core/fields/EditorField.tsx b/client/app/lib/components/core/fields/EditorField.tsx index 953023f138c..39c7fd3809b 100644 --- a/client/app/lib/components/core/fields/EditorField.tsx +++ b/client/app/lib/components/core/fields/EditorField.tsx @@ -1,8 +1,15 @@ -import { ComponentProps, ForwardedRef, forwardRef } from 'react'; +import { + ComponentProps, + ForwardedRef, + forwardRef, + useEffect, + useState, +} from 'react'; import AceEditor from 'react-ace'; import { LanguageMode } from 'types/course/assessment/question/programming'; import 'ace-builds/src-noconflict/theme-github'; +import 'ace-builds/src-noconflict/mode-python'; import './AceEditor.css'; @@ -30,11 +37,48 @@ const DEFAULT_FONT_FAMILY = [ 'monospace', ].join(','); +/** + * Loads Ace's mode scripts on demand for `language` and returns `true` if the mode + * has been loaded. + * + * Remember to update the regex in the `webpackInclude` comment below to bundle any + * new languages we support in the future. + */ +const useLazyMode = (language: LanguageMode): boolean => { + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + + let ignore = false; + + (async (): Promise => { + await import( + /* webpackInclude: /ace-builds\/src-noconflict\/mode-(c_cpp|python|r|java|javascript)\./ */ + /* webpackChunkName: "ace-[request]" */ + `ace-builds/src-noconflict/mode-${language}` + ); + + if (ignore) return; + + setLoading(false); + })(); + + return () => { + ignore = true; + }; + }, [language]); + + return loading; +}; + const EditorField = forwardRef( (props: EditorProps, ref: ForwardedRef): JSX.Element => { const { language, value, disabled, onChange, cursorStart, ...otherProps } = props; + const loading = useLazyMode(language); + return ( { classes: { root: 'rounded-full', sizeLarge: 'px-5 py-3', - sizeMedium: 'px-3 py-2', + sizeMedium: 'px-2 py-2', sizeSmall: 'min-w-[6rem] px-1 py-1', }, }, diff --git a/client/app/lib/initializers/ace-editor.js b/client/app/lib/initializers/ace-editor.js deleted file mode 100644 index 38d69c52d0c..00000000000 --- a/client/app/lib/initializers/ace-editor.js +++ /dev/null @@ -1,129 +0,0 @@ -const ace = require('ace-builds'); - -// Load ACE dependencies required for ACE syntax highlighting and theming -require('ace-builds/src-noconflict/mode-c_cpp'); -require('ace-builds/src-noconflict/mode-java'); -require('ace-builds/src-noconflict/mode-javascript'); -require('ace-builds/src-noconflict/mode-python'); -require('ace-builds/src-noconflict/mode-r'); -require('ace-builds/src-noconflict/theme-github'); - -/** - * Builds a new Ace editor container, with the given ID as a suffix. - * - * @param {String} id The ID to use as a suffix for the Ace editors. - * @returns {jQuery} - */ -function buildEditorContainer(id) { - const $editor = $('
'); - $editor.css({ - display: 'none', - position: 'relative', - }); - $editor[0].id = `ace_${id}`; - - return $editor; -} - -/** - * Builds the editor within the given container. - * - * @param {jQuery} $container The container to build the editor in. - * @param {Object} options The options for the editor. - * @returns {Object} The Ace editor instance. - */ -function buildEditor($container, options) { - const editor = ace.edit($container[0]); - editor.setTheme(`ace/theme/${options.theme}`); - editor.getSession().setMode(`ace/mode/${options.lang}`); - - editor.setOptions({ readOnly: !!options.readOnly }); - - return editor; -} - -/** - * Builds the Ace configuration object. - * - * @param {jQuery} $container The container for the Ace editor. - * @param {Object} aceEditor The Ace editor created. - * @returns {Object} - */ -function buildAceOptions($container, aceEditor) { - return { - container: $container, - editor: aceEditor, - }; -} - -/** - * Assigns the Ace configuration to the given element. - * - * @param {jQuery} $element The element which has the Ace editor associated with. - * @param {jQuery} $container The container element for the Ace editor. - * @param {Object} aceEditor The Ace editor created from `ace.edit`. - */ -function assignEditor($element, $container, aceEditor) { - $element.data('ace', buildAceOptions($container, aceEditor)); -} - -/** - * Finds or builds an editor container from the given jQuery element. - * - * @param {jQuery} $element The element to find or build an Ace editor for. - * @param {Object} options The options to use when building the element. - * @returns {jQuery} - */ -function findOrBuildEditorContainer($element, options) { - const aceData = $element.data('ace'); - if (aceData) { - return aceData.container; - } - - const $container = buildEditorContainer($element[0].id); - $container.insertAfter($element); - $container.height($element.height()); - - const editor = buildEditor($container, options); - editor.on('change', () => { - $element.val(editor.session.getValue()); - }); - editor.session.setValue($element.val()); - assignEditor($element, $container, editor); - - return $container; -} - -/** - * Redirects all labels from the old textarea to point to the new editor. - * @param {jQuery} $from The textarea to redirect the labels from. - * @param {jQuery} $editor The container for the Ace editor to redirect the labels to. - */ -function reassignLabelsToAce($from, $editor) { - const fromId = $from[0].id; - const $editorTextarea = $('textarea.ace_text-input', $editor); - $editorTextarea[0].id = `ace_textarea_${fromId}`; - $(`label[for="${fromId}"]`).attr('for', $editorTextarea[0].id); -} - -$.fn.ace = function (opt) { - const options = $.extend({}, $.fn.ace.defaults, opt); - return this.each(function () { - const $this = $(this); - const elementOptions = $.extend({}, options, { - lang: this.lang, - readOnly: $this[0].readOnly, - }); - const $editor = findOrBuildEditorContainer($this, elementOptions); - - $this.hide(); - $editor.show(); - - reassignLabelsToAce($this, $editor); - }); -}; - -$.fn.ace.defaults = { - theme: 'github', - lang: null, -}; diff --git a/client/webpack.common.js b/client/webpack.common.js index fcc8623d3d0..5b96da75f32 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -164,13 +164,6 @@ module.exports = { ], exclude: /node_modules/, }, - { - test: require.resolve('jquery'), - loader: 'expose-loader', - options: { - exposes: ['jQuery', '$'], - }, - }, { test: require.resolve('./app/lib/moment-timezone'), loader: 'expose-loader', diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 86c83b520d3..14832805b1c 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -31,7 +31,7 @@ def find_form(selector, action: nil) def wait_for_ajax Timeout.timeout(Capybara.default_max_wait_time) do - sleep 0.1 until page.evaluate_script('jQuery.active').zero? + sleep 0.1 end end From e6a6423bf8ce95e79bee8ceb76edf2538f26e001 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:57:58 +0800 Subject: [PATCH 4/4] fix(moment): import moment from lib/moment only, trim timezone data to >=2014 --- client/app/bundles/common/DashboardPage.tsx | 2 +- .../components/HeartbeatsTimeline.tsx | 2 +- .../components/HeartbeatsTimelineChart.tsx | 2 +- .../assessment/pages/AssessmentMonitoring/utils.ts | 2 +- .../LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx | 3 +-- .../submission/reducers/liveFeedbackChats/index.ts | 3 +-- .../components/DayCalendar/DayCalendar.tsx | 2 +- .../components/DayCalendar/DayColumn.tsx | 3 ++- .../reference-timelines/components/SubmitIndicator.tsx | 2 +- .../components/TimeBar/DurationBar.tsx | 3 ++- .../reference-timelines/components/TimeBar/TimeBar.tsx | 3 ++- .../components/TimeBar/TimeBarHandle.tsx | 3 ++- .../components/TimePopup/TimePopup.tsx | 2 +- .../components/TimePopup/TimePopupForm.tsx | 2 +- .../components/TimelinesStack/AssignableTimeline.tsx | 3 ++- .../components/TimelinesStack/Timeline.tsx | 2 +- .../reference-timelines/contexts/LastSavedContext.tsx | 3 ++- client/app/bundles/course/reference-timelines/utils.ts | 3 ++- .../reference-timelines/views/DayView/ItemsSidebar.tsx | 3 ++- client/package.json | 1 + client/webpack.prod.js | 2 ++ client/yarn.lock | 10 +++++++++- 22 files changed, 39 insertions(+), 22 deletions(-) diff --git a/client/app/bundles/common/DashboardPage.tsx b/client/app/bundles/common/DashboardPage.tsx index 4798315b944..be171e86977 100644 --- a/client/app/bundles/common/DashboardPage.tsx +++ b/client/app/bundles/common/DashboardPage.tsx @@ -2,7 +2,6 @@ import { defineMessages } from 'react-intl'; import { Navigate } from 'react-router-dom'; import { ArrowForward } from '@mui/icons-material'; import { Avatar, Stack, Typography } from '@mui/material'; -import moment from 'moment'; import { HomeLayoutCourseData } from 'types/home'; import { getCourseLogoUrl } from 'course/helper'; @@ -13,6 +12,7 @@ import { useAppContext } from 'lib/containers/AppContainer'; import { getUrlParameter } from 'lib/helpers/url-helpers'; import useItems from 'lib/hooks/items/useItems'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import NewCourseButton from './components/NewCourseButton'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx index ea2939f22d5..e14221272c6 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimeline.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; -import moment from 'moment'; import { HeartbeatDetail } from 'types/channels/liveMonitoring'; import { BrowserAuthorizationMethod } from 'course/assessment/components/monitoring/BrowserAuthorizationMethodOptionsFormFields/common'; +import moment from 'lib/moment'; import HeartbeatDetailCard from './HeartbeatDetailCard'; import HeartbeatsTimelineChart from './HeartbeatsTimelineChart'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx index 3e47f456841..68af626e572 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/components/HeartbeatsTimelineChart.tsx @@ -10,12 +10,12 @@ import { PointStyle, } from 'chart.js'; import zoomPlugin from 'chartjs-plugin-zoom'; -import moment from 'moment'; import palette from 'theme/palette'; import { HeartbeatDetail } from 'types/channels/liveMonitoring'; import { useAppSelector } from 'lib/hooks/store'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../../translations'; import { select } from '../selectors'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts index 3ab5a503a8a..ca23d6dc429 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts +++ b/client/app/bundles/course/assessment/pages/AssessmentMonitoring/utils.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import moment from 'lib/moment'; export type Presence = 'alive' | 'late' | 'missing'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx index 7860f3ca075..fa9f6f7b250 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackMessageHistory.tsx @@ -1,6 +1,5 @@ import { Dispatch, FC, SetStateAction } from 'react'; import { Typography } from '@mui/material'; -import moment from 'moment'; import { LiveFeedbackChatMessage } from 'types/course/assessment/submission/liveFeedback'; import { @@ -8,7 +7,7 @@ import { justifyPosition, } from 'course/assessment/submission/components/GetHelpChatPage/utils'; import MarkdownText from 'course/assessment/submission/components/MarkdownText'; -import { SHORT_DATE_TIME_FORMAT } from 'lib/moment'; +import moment, { SHORT_DATE_TIME_FORMAT } from 'lib/moment'; interface Props { messages: LiveFeedbackChatMessage[]; diff --git a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts index 60f12b53400..b5e115df408 100644 --- a/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts +++ b/client/app/bundles/course/assessment/submission/reducers/liveFeedbackChats/index.ts @@ -5,9 +5,8 @@ import { PayloadAction, } from '@reduxjs/toolkit'; import { shuffle } from 'lodash'; -import moment from 'moment'; -import { SHORT_TIME_FORMAT } from 'lib/moment'; +import moment, { SHORT_TIME_FORMAT } from 'lib/moment'; import { getLocalStorageValue, diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx index 719499bdd2c..d3ba32d4527 100644 --- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx +++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayCalendar.tsx @@ -2,9 +2,9 @@ import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import { Button, Typography } from '@mui/material'; -import moment from 'moment'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../translations'; import { diff --git a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx index 4ca273feeb8..7cee1062211 100644 --- a/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx +++ b/client/app/bundles/course/reference-timelines/components/DayCalendar/DayColumn.tsx @@ -1,7 +1,8 @@ import { CSSProperties, memo } from 'react'; import { areEqual } from 'react-window'; import { Typography } from '@mui/material'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { getSecondsFromDays, isToday, isWeekend } from '../../utils'; diff --git a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx index 416fd39637f..8d77d7dca2b 100644 --- a/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx +++ b/client/app/bundles/course/reference-timelines/components/SubmitIndicator.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; import { Cancel, CheckCircle } from '@mui/icons-material'; import { Chip, Grow, Tooltip, Typography } from '@mui/material'; -import moment from 'moment'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import { useLastSaved } from '../contexts'; import translations from '../translations'; diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx index b5b64dc5b89..82d74dd9123 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/DurationBar.tsx @@ -1,5 +1,6 @@ import { MouseEventHandler, ReactNode, TouchEventHandler } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { DAY_WIDTH_PIXELS, diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx index f2d206374ed..9dbae1ef05a 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBar.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; import { DAY_WIDTH_PIXELS, getDaysFromWidth } from '../../utils'; import HorizontallyDraggable from '../HorizontallyDraggable'; diff --git a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx index 9c5c2a28c03..b6cd8a2ac82 100644 --- a/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimeBar/TimeBarHandle.tsx @@ -5,7 +5,8 @@ import { TouchEventHandler, } from 'react'; import { Typography } from '@mui/material'; -import moment from 'moment'; + +import moment from 'lib/moment'; interface HandleContentProps { side: 'start' | 'end'; diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx index ceb9814270b..209aca0c70b 100644 --- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx @@ -1,5 +1,4 @@ import { Typography } from '@mui/material'; -import moment from 'moment'; import { ItemWithTimeData, TimelineData, @@ -8,6 +7,7 @@ import { import { useAppDispatch } from 'lib/hooks/store'; import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import { useSetLastSaved } from '../../contexts'; import { createTime, deleteTime, updateTime } from '../../operations'; diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx index b7a7309ffa8..bdb650773d4 100644 --- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopupForm.tsx @@ -1,11 +1,11 @@ import { Controller } from 'react-hook-form'; import { Button, Collapse } from '@mui/material'; -import moment from 'moment'; import { date, object, ref } from 'yup'; import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField'; import Form from 'lib/components/form/Form'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import formTranslations from 'lib/translations/form'; import { useLastSaved } from '../../contexts'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx index 989b619bf31..1256c169631 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignableTimeline.tsx @@ -1,11 +1,12 @@ import { useState } from 'react'; -import moment from 'moment'; import { ItemWithTimeData, TimeData, TimelineData, } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + import { useLastSaved } from '../../contexts'; import { DraftableTimeData } from '../../utils'; import TimePopup from '../TimePopup'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx index c24c262ccc8..9cb7223ab5b 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/Timeline.tsx @@ -1,10 +1,10 @@ import { ReactNode, useState } from 'react'; import { Add } from '@mui/icons-material'; import { Typography } from '@mui/material'; -import moment from 'moment'; import { TimeData } from 'types/course/referenceTimelines'; import useTranslation from 'lib/hooks/useTranslation'; +import moment from 'lib/moment'; import translations from '../../translations'; import { DAY_WIDTH_PIXELS, getSecondsFromDays } from '../../utils'; diff --git a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx index c868b65d4ba..873433794ad 100644 --- a/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx +++ b/client/app/bundles/course/reference-timelines/contexts/LastSavedContext.tsx @@ -6,7 +6,8 @@ import { useContext, useState, } from 'react'; -import moment from 'moment'; + +import moment from 'lib/moment'; type FetchStatus = 'loading' | 'success' | 'failure'; diff --git a/client/app/bundles/course/reference-timelines/utils.ts b/client/app/bundles/course/reference-timelines/utils.ts index b10859b880e..9cce968a80b 100644 --- a/client/app/bundles/course/reference-timelines/utils.ts +++ b/client/app/bundles/course/reference-timelines/utils.ts @@ -1,6 +1,7 @@ -import moment from 'moment'; import { TimeData } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + const SECONDS_IN_A_DAY = 86_400 as const; export const DAY_WIDTH_PIXELS = 30 as const; diff --git a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx index e0821f4979e..12081c2be64 100644 --- a/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx +++ b/client/app/bundles/course/reference-timelines/views/DayView/ItemsSidebar.tsx @@ -1,10 +1,11 @@ import { Typography } from '@mui/material'; -import moment from 'moment'; import { ItemWithTimeData, TimelineData, } from 'types/course/referenceTimelines'; +import moment from 'lib/moment'; + import { getDaysFromSeconds } from '../../utils'; import TimelineSidebarItem from './TimelineSidebarItem'; diff --git a/client/package.json b/client/package.json index da4c7900aa6..0a6e9980fb0 100644 --- a/client/package.json +++ b/client/package.json @@ -181,6 +181,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-localstorage-mock": "^2.4.26", "mkdirp": "^3.0.1", + "moment-timezone-data-webpack-plugin": "^1.5.1", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", "prettier": "^3.2.5", diff --git a/client/webpack.prod.js b/client/webpack.prod.js index 84d484a03c3..348de3d8e0b 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -1,5 +1,6 @@ const { merge } = require('webpack-merge'); const TerserPlugin = require('terser-webpack-plugin'); +const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin'); const common = require('./webpack.common'); @@ -29,4 +30,5 @@ module.exports = merge(common, { }, ], }, + plugins: [new MomentTimezoneDataPlugin({ startYear: 2014 })], }); diff --git a/client/yarn.lock b/client/yarn.lock index 1da56906603..8809c61de32 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5592,7 +5592,7 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.3.2: +find-cache-dir@^3.0.0, find-cache-dir@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== @@ -8004,6 +8004,14 @@ mkdirp@^3.0.1: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== +moment-timezone-data-webpack-plugin@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.5.1.tgz#9d35dfd3768db55058e1e809d77a2b64bd6d03a4" + integrity sha512-1le6a35GgYdWMVYFzrfpE/F6Pk4bj0M3QKD6Iv6ba9LqWGoVqHQRHyCTLvLis5E1J98Sz40ET6yhZzMVakwpjg== + dependencies: + find-cache-dir "^3.0.0" + make-dir "^3.0.0" + moment-timezone@^0.5.44: version "0.5.44" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.44.tgz#a64a4e47b68a43deeab5ae4eb4f82da77cdf595f"