From 0a2f42b4233ad3a6ed8b7b9217cb90175e544a7b Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Tue, 12 Sep 2023 03:08:02 +0800 Subject: [PATCH 01/13] Relocate side content --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 8 ++++---- src/commons/editingWorkspace/EditingWorkspace.tsx | 2 +- .../sideContent/__tests__/SideContentAutograder.tsx | 2 +- .../sideContent/__tests__/SideContentHtmlDisplay.tsx | 2 +- .../{ => content}/SideContentAutograder.tsx | 8 ++++---- .../{ => content}/SideContentCanvasOutput.tsx | 0 .../{ => content}/SideContentContestLeaderboard.tsx | 2 +- .../{ => content}/SideContentContestVoting.tsx | 2 +- .../SideContentContestVotingContainer.tsx | 2 +- .../{ => content}/SideContentDataVisualizer.tsx | 6 +++--- .../{ => content}/SideContentEnvVisualizer.tsx | 12 ++++++------ .../{ => content}/SideContentFaceapiDisplay.tsx | 0 .../{ => content}/SideContentHtmlDisplay.tsx | 0 .../{ => content}/SideContentLeaderboardCard.tsx | 2 +- .../{ => content}/SideContentResultCard.tsx | 2 +- .../{ => content}/SideContentSubstVisualizer.tsx | 0 .../{ => content}/SideContentTestcaseCard.tsx | 4 ++-- .../{ => content}/SideContentToneMatrix.tsx | 0 .../SideContentEditableTestcaseCard.tsx | 2 +- .../githubAssessments/SideContentMarkdownEditor.tsx | 2 +- .../githubAssessments/SideContentMissionEditor.tsx | 8 ++++---- .../githubAssessments/SideContentTaskEditor.tsx | 0 .../githubAssessments/SideContentTestcaseEditor.tsx | 6 +++--- .../remoteExecution/DeviceMenuItemButtons.tsx | 0 .../remoteExecution/SideContentRemoteExecution.tsx | 6 +++--- .../grading/subcomponents/GradingWorkspace.tsx | 4 ++-- src/pages/academy/sourcereel/Sourcereel.tsx | 4 ++-- .../githubAssessments/GitHubAssessmentWorkspace.tsx | 8 ++++---- src/pages/playground/PlaygroundTabs.tsx | 12 ++++++------ src/pages/sourcecast/Sourcecast.tsx | 4 ++-- 30 files changed, 55 insertions(+), 55 deletions(-) rename src/commons/sideContent/{ => content}/SideContentAutograder.tsx (94%) rename src/commons/sideContent/{ => content}/SideContentCanvasOutput.tsx (100%) rename src/commons/sideContent/{ => content}/SideContentContestLeaderboard.tsx (97%) rename src/commons/sideContent/{ => content}/SideContentContestVoting.tsx (99%) rename src/commons/sideContent/{ => content}/SideContentContestVotingContainer.tsx (97%) rename src/commons/sideContent/{ => content}/SideContentDataVisualizer.tsx (96%) rename src/commons/sideContent/{ => content}/SideContentEnvVisualizer.tsx (97%) rename src/commons/sideContent/{ => content}/SideContentFaceapiDisplay.tsx (100%) rename src/commons/sideContent/{ => content}/SideContentHtmlDisplay.tsx (100%) rename src/commons/sideContent/{ => content}/SideContentLeaderboardCard.tsx (94%) rename src/commons/sideContent/{ => content}/SideContentResultCard.tsx (95%) rename src/commons/sideContent/{ => content}/SideContentSubstVisualizer.tsx (100%) rename src/commons/sideContent/{ => content}/SideContentTestcaseCard.tsx (95%) rename src/commons/sideContent/{ => content}/SideContentToneMatrix.tsx (100%) rename src/commons/sideContent/{ => content}/githubAssessments/SideContentEditableTestcaseCard.tsx (97%) rename src/commons/sideContent/{ => content}/githubAssessments/SideContentMarkdownEditor.tsx (97%) rename src/commons/sideContent/{ => content}/githubAssessments/SideContentMissionEditor.tsx (84%) rename src/commons/sideContent/{ => content}/githubAssessments/SideContentTaskEditor.tsx (100%) rename src/commons/sideContent/{ => content}/githubAssessments/SideContentTestcaseEditor.tsx (97%) rename src/commons/sideContent/{ => content}/remoteExecution/DeviceMenuItemButtons.tsx (100%) rename src/commons/sideContent/{ => content}/remoteExecution/SideContentRemoteExecution.tsx (97%) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index bd6a6f94b7..a6522697d2 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -58,11 +58,11 @@ import { Position } from '../editor/EditorTypes'; import Markdown from '../Markdown'; import { MobileSideContentProps } from '../mobileWorkspace/mobileSideContent/MobileSideContent'; import MobileWorkspace, { MobileWorkspaceProps } from '../mobileWorkspace/MobileWorkspace'; +import SideContentAutograder from '../sideContent/content/SideContentAutograder'; +import SideContentContestLeaderboard from '../sideContent/content/SideContentContestLeaderboard'; +import SideContentContestVotingContainer from '../sideContent/content/SideContentContestVotingContainer'; +import SideContentToneMatrix from '../sideContent/content/SideContentToneMatrix'; import { SideContentProps } from '../sideContent/SideContent'; -import SideContentAutograder from '../sideContent/SideContentAutograder'; -import SideContentContestLeaderboard from '../sideContent/SideContentContestLeaderboard'; -import SideContentContestVotingContainer from '../sideContent/SideContentContestVotingContainer'; -import SideContentToneMatrix from '../sideContent/SideContentToneMatrix'; import { SideContentTab, SideContentType } from '../sideContent/SideContentTypes'; import Constants from '../utils/Constants'; import { useResponsive, useTypedSelector } from '../utils/Hooks'; diff --git a/src/commons/editingWorkspace/EditingWorkspace.tsx b/src/commons/editingWorkspace/EditingWorkspace.tsx index ff84f95e69..bef8fd0c8b 100644 --- a/src/commons/editingWorkspace/EditingWorkspace.tsx +++ b/src/commons/editingWorkspace/EditingWorkspace.tsx @@ -46,8 +46,8 @@ import { TextAreaContent } from '../editingWorkspaceSideContent/EditingWorkspace import { convertEditorTabStateToProps } from '../editor/EditorContainer'; import { Position } from '../editor/EditorTypes'; import Markdown from '../Markdown'; +import SideContentToneMatrix from '../sideContent/content/SideContentToneMatrix'; import { SideContentProps } from '../sideContent/SideContent'; -import SideContentToneMatrix from '../sideContent/SideContentToneMatrix'; import { SideContentTab, SideContentType } from '../sideContent/SideContentTypes'; import { useTypedSelector } from '../utils/Hooks'; import Workspace, { WorkspaceProps } from '../workspace/Workspace'; diff --git a/src/commons/sideContent/__tests__/SideContentAutograder.tsx b/src/commons/sideContent/__tests__/SideContentAutograder.tsx index 023c9636bf..883cdc8783 100644 --- a/src/commons/sideContent/__tests__/SideContentAutograder.tsx +++ b/src/commons/sideContent/__tests__/SideContentAutograder.tsx @@ -4,7 +4,7 @@ import { shallowRender } from 'src/commons/utils/TestUtils'; import { AutogradingResult, Testcase, TestcaseTypes } from '../../assessment/AssessmentTypes'; import { mockGrading } from '../../mocks/GradingMocks'; -import SideContentAutograder, { SideContentAutograderProps } from '../SideContentAutograder'; +import SideContentAutograder, { SideContentAutograderProps } from '../content/SideContentAutograder'; const mockErrors: SourceError[] = [ { diff --git a/src/commons/sideContent/__tests__/SideContentHtmlDisplay.tsx b/src/commons/sideContent/__tests__/SideContentHtmlDisplay.tsx index ea72d546e6..5e878000c4 100644 --- a/src/commons/sideContent/__tests__/SideContentHtmlDisplay.tsx +++ b/src/commons/sideContent/__tests__/SideContentHtmlDisplay.tsx @@ -2,7 +2,7 @@ import { fireEvent, render } from '@testing-library/react'; import { stringify } from 'js-slang/dist/utils/stringify'; import { renderTreeJson } from 'src/commons/utils/TestUtils'; -import SideContentHtmlDisplay from '../SideContentHtmlDisplay'; +import SideContentHtmlDisplay from '../content/SideContentHtmlDisplay'; test('HTML Display renders correctly', () => { const mockProps = { diff --git a/src/commons/sideContent/SideContentAutograder.tsx b/src/commons/sideContent/content/SideContentAutograder.tsx similarity index 94% rename from src/commons/sideContent/SideContentAutograder.tsx rename to src/commons/sideContent/content/SideContentAutograder.tsx index 5b11670d46..73dda47141 100644 --- a/src/commons/sideContent/SideContentAutograder.tsx +++ b/src/commons/sideContent/content/SideContentAutograder.tsx @@ -3,11 +3,11 @@ import { IconNames } from '@blueprintjs/icons'; import { Tooltip2 } from '@blueprintjs/popover2'; import * as React from 'react'; -import { AutogradingResult, Testcase } from '../assessment/AssessmentTypes'; -import ControlButton from '../ControlButton'; -import { WorkspaceLocation } from '../workspace/WorkspaceTypes'; +import { AutogradingResult, Testcase } from '../../assessment/AssessmentTypes'; +import ControlButton from '../../ControlButton'; +import { WorkspaceLocation } from '../../workspace/WorkspaceTypes'; import SideContentResultCard from './SideContentResultCard'; -import SideContentTestcaseCard from './SideContentTestcaseCard'; +import SideContentTestcaseCard from './SideContentTestcaseCard'; export type SideContentAutograderProps = DispatchProps & StateProps & OwnProps; diff --git a/src/commons/sideContent/SideContentCanvasOutput.tsx b/src/commons/sideContent/content/SideContentCanvasOutput.tsx similarity index 100% rename from src/commons/sideContent/SideContentCanvasOutput.tsx rename to src/commons/sideContent/content/SideContentCanvasOutput.tsx diff --git a/src/commons/sideContent/SideContentContestLeaderboard.tsx b/src/commons/sideContent/content/SideContentContestLeaderboard.tsx similarity index 97% rename from src/commons/sideContent/SideContentContestLeaderboard.tsx rename to src/commons/sideContent/content/SideContentContestLeaderboard.tsx index 85bc9d7311..a024b99f40 100644 --- a/src/commons/sideContent/SideContentContestLeaderboard.tsx +++ b/src/commons/sideContent/content/SideContentContestLeaderboard.tsx @@ -3,7 +3,7 @@ import { IconNames } from '@blueprintjs/icons'; import { Tooltip2 } from '@blueprintjs/popover2'; import React, { useMemo, useState } from 'react'; -import { ContestEntry } from '../assessment/AssessmentTypes'; +import { ContestEntry } from '../../assessment/AssessmentTypes'; import SideContentLeaderboardCard from './SideContentLeaderboardCard'; export type SideContentContestLeaderboardProps = DispatchProps & StateProps; diff --git a/src/commons/sideContent/SideContentContestVoting.tsx b/src/commons/sideContent/content/SideContentContestVoting.tsx similarity index 99% rename from src/commons/sideContent/SideContentContestVoting.tsx rename to src/commons/sideContent/content/SideContentContestVoting.tsx index 55d16ca48e..15b58cb9a9 100644 --- a/src/commons/sideContent/SideContentContestVoting.tsx +++ b/src/commons/sideContent/content/SideContentContestVoting.tsx @@ -4,7 +4,7 @@ import { Tooltip2 } from '@blueprintjs/popover2'; import classNames from 'classnames'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { ContestEntry } from '../assessment/AssessmentTypes'; +import { ContestEntry } from '../../assessment/AssessmentTypes'; export type SideContentContestVotingProps = DispatchProps & StateProps; diff --git a/src/commons/sideContent/SideContentContestVotingContainer.tsx b/src/commons/sideContent/content/SideContentContestVotingContainer.tsx similarity index 97% rename from src/commons/sideContent/SideContentContestVotingContainer.tsx rename to src/commons/sideContent/content/SideContentContestVotingContainer.tsx index 90712f83e7..8aaa379a37 100644 --- a/src/commons/sideContent/SideContentContestVotingContainer.tsx +++ b/src/commons/sideContent/content/SideContentContestVotingContainer.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { showWarningMessage } from 'src/commons/utils/notifications/NotificationsHelper'; -import { ContestEntry } from '../assessment/AssessmentTypes'; +import { ContestEntry } from '../../assessment/AssessmentTypes'; import SideContentContestVoting from './SideContentContestVoting'; export type SideContentContestVotingContainerProps = DispatchProps & StateProps; diff --git a/src/commons/sideContent/SideContentDataVisualizer.tsx b/src/commons/sideContent/content/SideContentDataVisualizer.tsx similarity index 96% rename from src/commons/sideContent/SideContentDataVisualizer.tsx rename to src/commons/sideContent/content/SideContentDataVisualizer.tsx index a91554f92a..b1988b00b6 100644 --- a/src/commons/sideContent/SideContentDataVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentDataVisualizer.tsx @@ -3,10 +3,10 @@ import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import * as React from 'react'; import { configure, GlobalHotKeys } from 'react-hotkeys'; +import DataVisualizer from 'src/features/dataVisualizer/dataVisualizer'; +import { Step } from 'src/features/dataVisualizer/dataVisualizerTypes'; -import DataVisualizer from '../../features/dataVisualizer/dataVisualizer'; -import { Step } from '../../features/dataVisualizer/dataVisualizerTypes'; -import { Links } from '../utils/Constants'; +import { Links } from '../../utils/Constants'; type State = { steps: Step[]; diff --git a/src/commons/sideContent/SideContentEnvVisualizer.tsx b/src/commons/sideContent/content/SideContentEnvVisualizer.tsx similarity index 97% rename from src/commons/sideContent/SideContentEnvVisualizer.tsx rename to src/commons/sideContent/content/SideContentEnvVisualizer.tsx index b8873da2f2..131a1554d0 100644 --- a/src/commons/sideContent/SideContentEnvVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentEnvVisualizer.tsx @@ -17,12 +17,12 @@ import { bindActionCreators, Dispatch } from 'redux'; import EnvVisualizer from 'src/features/envVisualizer/EnvVisualizer'; import { Layout } from 'src/features/envVisualizer/EnvVisualizerLayout'; -import { OverallState } from '../application/ApplicationTypes'; -import { HighlightedLines } from '../editor/EditorTypes'; -import Constants, { Links } from '../utils/Constants'; -import { setEditorHighlightedLinesAgenda, updateEnvSteps } from '../workspace/WorkspaceActions'; -import { evalEditor } from '../workspace/WorkspaceActions'; -import { WorkspaceLocation } from '../workspace/WorkspaceTypes'; +import { OverallState } from '../../application/ApplicationTypes'; +import { HighlightedLines } from '../../editor/EditorTypes'; +import Constants, { Links } from '../../utils/Constants'; +import { setEditorHighlightedLinesAgenda, updateEnvSteps } from '../../workspace/WorkspaceActions'; +import { evalEditor } from '../../workspace/WorkspaceActions'; +import { WorkspaceLocation } from '../../workspace/WorkspaceTypes'; type State = { visualization: React.ReactNode; diff --git a/src/commons/sideContent/SideContentFaceapiDisplay.tsx b/src/commons/sideContent/content/SideContentFaceapiDisplay.tsx similarity index 100% rename from src/commons/sideContent/SideContentFaceapiDisplay.tsx rename to src/commons/sideContent/content/SideContentFaceapiDisplay.tsx diff --git a/src/commons/sideContent/SideContentHtmlDisplay.tsx b/src/commons/sideContent/content/SideContentHtmlDisplay.tsx similarity index 100% rename from src/commons/sideContent/SideContentHtmlDisplay.tsx rename to src/commons/sideContent/content/SideContentHtmlDisplay.tsx diff --git a/src/commons/sideContent/SideContentLeaderboardCard.tsx b/src/commons/sideContent/content/SideContentLeaderboardCard.tsx similarity index 94% rename from src/commons/sideContent/SideContentLeaderboardCard.tsx rename to src/commons/sideContent/content/SideContentLeaderboardCard.tsx index 23548dbbe0..77abb98ab5 100644 --- a/src/commons/sideContent/SideContentLeaderboardCard.tsx +++ b/src/commons/sideContent/content/SideContentLeaderboardCard.tsx @@ -2,7 +2,7 @@ import { Card, Classes, Elevation, Pre } from '@blueprintjs/core'; import classNames from 'classnames'; import React from 'react'; -import { ContestEntry } from '../assessment/AssessmentTypes'; +import { ContestEntry } from '../../assessment/AssessmentTypes'; type SideContentLeaderboardCardProps = DispatchProps & StateProps; diff --git a/src/commons/sideContent/SideContentResultCard.tsx b/src/commons/sideContent/content/SideContentResultCard.tsx similarity index 95% rename from src/commons/sideContent/SideContentResultCard.tsx rename to src/commons/sideContent/content/SideContentResultCard.tsx index 3502538b29..81e1640de3 100644 --- a/src/commons/sideContent/SideContentResultCard.tsx +++ b/src/commons/sideContent/content/SideContentResultCard.tsx @@ -2,7 +2,7 @@ import { Card, Elevation, Pre } from '@blueprintjs/core'; import classNames from 'classnames'; import * as React from 'react'; -import { AutogradingError, AutogradingResult } from '../assessment/AssessmentTypes'; +import { AutogradingError, AutogradingResult } from '../../assessment/AssessmentTypes'; type SideContentResultCardProps = StateProps; diff --git a/src/commons/sideContent/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx similarity index 100% rename from src/commons/sideContent/SideContentSubstVisualizer.tsx rename to src/commons/sideContent/content/SideContentSubstVisualizer.tsx diff --git a/src/commons/sideContent/SideContentTestcaseCard.tsx b/src/commons/sideContent/content/SideContentTestcaseCard.tsx similarity index 95% rename from src/commons/sideContent/SideContentTestcaseCard.tsx rename to src/commons/sideContent/content/SideContentTestcaseCard.tsx index 2a35ffb0fa..5b11c39f48 100644 --- a/src/commons/sideContent/SideContentTestcaseCard.tsx +++ b/src/commons/sideContent/content/SideContentTestcaseCard.tsx @@ -4,8 +4,8 @@ import { parseError } from 'js-slang'; import { stringify } from 'js-slang/dist/utils/stringify'; import * as React from 'react'; -import { Testcase, TestcaseTypes } from '../assessment/AssessmentTypes'; -import { WorkspaceLocation } from '../workspace/WorkspaceTypes'; +import { Testcase, TestcaseTypes } from '../../assessment/AssessmentTypes'; +import { WorkspaceLocation } from '../../workspace/WorkspaceTypes'; type SideContentTestcaseCardProps = DispatchProps & StateProps & OwnProps; diff --git a/src/commons/sideContent/SideContentToneMatrix.tsx b/src/commons/sideContent/content/SideContentToneMatrix.tsx similarity index 100% rename from src/commons/sideContent/SideContentToneMatrix.tsx rename to src/commons/sideContent/content/SideContentToneMatrix.tsx diff --git a/src/commons/sideContent/githubAssessments/SideContentEditableTestcaseCard.tsx b/src/commons/sideContent/content/githubAssessments/SideContentEditableTestcaseCard.tsx similarity index 97% rename from src/commons/sideContent/githubAssessments/SideContentEditableTestcaseCard.tsx rename to src/commons/sideContent/content/githubAssessments/SideContentEditableTestcaseCard.tsx index 4ffb93d57d..d4b64f925e 100644 --- a/src/commons/sideContent/githubAssessments/SideContentEditableTestcaseCard.tsx +++ b/src/commons/sideContent/content/githubAssessments/SideContentEditableTestcaseCard.tsx @@ -5,7 +5,7 @@ import { parseError } from 'js-slang'; import { stringify } from 'js-slang/dist/utils/stringify'; import * as React from 'react'; -import { Testcase, TestcaseTypes } from '../../assessment/AssessmentTypes'; +import { Testcase, TestcaseTypes } from '../../../assessment/AssessmentTypes'; type SideContentEditableTestcaseCardProps = DispatchProps & StateProps; diff --git a/src/commons/sideContent/githubAssessments/SideContentMarkdownEditor.tsx b/src/commons/sideContent/content/githubAssessments/SideContentMarkdownEditor.tsx similarity index 97% rename from src/commons/sideContent/githubAssessments/SideContentMarkdownEditor.tsx rename to src/commons/sideContent/content/githubAssessments/SideContentMarkdownEditor.tsx index d4b6be3625..438d5de7b3 100644 --- a/src/commons/sideContent/githubAssessments/SideContentMarkdownEditor.tsx +++ b/src/commons/sideContent/content/githubAssessments/SideContentMarkdownEditor.tsx @@ -1,7 +1,7 @@ import { TextArea } from '@blueprintjs/core'; import React, { useEffect } from 'react'; -import Markdown from '../../Markdown'; +import Markdown from '../../../Markdown'; export type SideContentMarkdownEditorProps = { allowEdits: boolean; diff --git a/src/commons/sideContent/githubAssessments/SideContentMissionEditor.tsx b/src/commons/sideContent/content/githubAssessments/SideContentMissionEditor.tsx similarity index 84% rename from src/commons/sideContent/githubAssessments/SideContentMissionEditor.tsx rename to src/commons/sideContent/content/githubAssessments/SideContentMissionEditor.tsx index 2705e57179..57b3aeaec0 100644 --- a/src/commons/sideContent/githubAssessments/SideContentMissionEditor.tsx +++ b/src/commons/sideContent/content/githubAssessments/SideContentMissionEditor.tsx @@ -2,10 +2,10 @@ import { Label } from '@blueprintjs/core'; import { Chapter } from 'js-slang/dist/types'; import React from 'react'; -import { SALanguage } from '../../application/ApplicationTypes'; -import { ControlBarChapterSelect } from '../../controlBar/ControlBarChapterSelect'; -import { MissionMetadata } from '../../githubAssessments/GitHubMissionTypes'; -import Constants from '../../utils/Constants'; +import { SALanguage } from '../../../application/ApplicationTypes'; +import { ControlBarChapterSelect } from '../../../controlBar/ControlBarChapterSelect'; +import { MissionMetadata } from '../../../githubAssessments/GitHubMissionTypes'; +import Constants from '../../../utils/Constants'; export type SideContentMissionEditorProps = { isFolderModeEnabled: boolean; diff --git a/src/commons/sideContent/githubAssessments/SideContentTaskEditor.tsx b/src/commons/sideContent/content/githubAssessments/SideContentTaskEditor.tsx similarity index 100% rename from src/commons/sideContent/githubAssessments/SideContentTaskEditor.tsx rename to src/commons/sideContent/content/githubAssessments/SideContentTaskEditor.tsx diff --git a/src/commons/sideContent/githubAssessments/SideContentTestcaseEditor.tsx b/src/commons/sideContent/content/githubAssessments/SideContentTestcaseEditor.tsx similarity index 97% rename from src/commons/sideContent/githubAssessments/SideContentTestcaseEditor.tsx rename to src/commons/sideContent/content/githubAssessments/SideContentTestcaseEditor.tsx index 34d3f93c28..b0571d87e8 100644 --- a/src/commons/sideContent/githubAssessments/SideContentTestcaseEditor.tsx +++ b/src/commons/sideContent/content/githubAssessments/SideContentTestcaseEditor.tsx @@ -4,9 +4,9 @@ import { Tooltip2 } from '@blueprintjs/popover2'; import * as React from 'react'; import AceEditor from 'react-ace'; -import { Testcase } from '../../assessment/AssessmentTypes'; -import ControlButton from '../../ControlButton'; -import { showSimpleConfirmDialog } from '../../utils/DialogHelper'; +import { Testcase } from '../../../assessment/AssessmentTypes'; +import ControlButton from '../../../ControlButton'; +import { showSimpleConfirmDialog } from '../../../utils/DialogHelper'; import SideContentTestcaseCard from '../SideContentTestcaseCard'; import SideContentEditableTestcaseCard from './SideContentEditableTestcaseCard'; diff --git a/src/commons/sideContent/remoteExecution/DeviceMenuItemButtons.tsx b/src/commons/sideContent/content/remoteExecution/DeviceMenuItemButtons.tsx similarity index 100% rename from src/commons/sideContent/remoteExecution/DeviceMenuItemButtons.tsx rename to src/commons/sideContent/content/remoteExecution/DeviceMenuItemButtons.tsx diff --git a/src/commons/sideContent/remoteExecution/SideContentRemoteExecution.tsx b/src/commons/sideContent/content/remoteExecution/SideContentRemoteExecution.tsx similarity index 97% rename from src/commons/sideContent/remoteExecution/SideContentRemoteExecution.tsx rename to src/commons/sideContent/content/remoteExecution/SideContentRemoteExecution.tsx index 48ed1b929e..6af8e410fa 100644 --- a/src/commons/sideContent/remoteExecution/SideContentRemoteExecution.tsx +++ b/src/commons/sideContent/content/remoteExecution/SideContentRemoteExecution.tsx @@ -24,9 +24,9 @@ import { } from 'src/features/remoteExecution/RemoteExecutionEv3Types'; import { Device, DeviceSession } from 'src/features/remoteExecution/RemoteExecutionTypes'; -import { actions } from '../../utils/ActionsHelper'; -import { useTypedSelector } from '../../utils/Hooks'; -import { WorkspaceLocation } from '../../workspace/WorkspaceTypes'; +import { actions } from '../../../utils/ActionsHelper'; +import { useTypedSelector } from '../../../utils/Hooks'; +import { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; import DeviceMenuItemButtons from './DeviceMenuItemButtons'; interface SideContentRemoteExecutionProps { diff --git a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx index 8c15ef3c1d..89c1675ab5 100644 --- a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx +++ b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router'; import { fetchGrading } from 'src/commons/application/actions/SessionActions'; -import SideContentToneMatrix from 'src/commons/sideContent/SideContentToneMatrix'; +import SideContentToneMatrix from 'src/commons/sideContent/content/SideContentToneMatrix'; import { showSimpleErrorDialog } from 'src/commons/utils/DialogHelper'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { @@ -51,8 +51,8 @@ import { ControlBarRunButton } from '../../../../commons/controlBar/ControlBarRu import { convertEditorTabStateToProps } from '../../../../commons/editor/EditorContainer'; import { Position } from '../../../../commons/editor/EditorTypes'; import Markdown from '../../../../commons/Markdown'; +import SideContentAutograder from '../../../../commons/sideContent/content/SideContentAutograder'; import { SideContentProps } from '../../../../commons/sideContent/SideContent'; -import SideContentAutograder from '../../../../commons/sideContent/SideContentAutograder'; import { SideContentTab, SideContentType } from '../../../../commons/sideContent/SideContentTypes'; import Workspace, { WorkspaceProps } from '../../../../commons/workspace/Workspace'; import { WorkspaceLocation, WorkspaceState } from '../../../../commons/workspace/WorkspaceTypes'; diff --git a/src/pages/academy/sourcereel/Sourcereel.tsx b/src/pages/academy/sourcereel/Sourcereel.tsx index 54d68d852c..e3cc5a408d 100644 --- a/src/pages/academy/sourcereel/Sourcereel.tsx +++ b/src/pages/academy/sourcereel/Sourcereel.tsx @@ -42,8 +42,8 @@ import { SourcecastEditorContainerProps } from '../../../commons/editor/EditorContainer'; import { Position } from '../../../commons/editor/EditorTypes'; -import SideContentDataVisualizer from '../../../commons/sideContent/SideContentDataVisualizer'; -import SideContentEnvVisualizer from '../../../commons/sideContent/SideContentEnvVisualizer'; +import SideContentDataVisualizer from '../../../commons/sideContent/content/SideContentDataVisualizer'; +import SideContentEnvVisualizer from '../../../commons/sideContent/content/SideContentEnvVisualizer'; import { SideContentTab, SideContentType } from '../../../commons/sideContent/SideContentTypes'; import SourceRecorderControlBar, { SourceRecorderControlBarProps diff --git a/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx b/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx index 18db93619c..91682d69ff 100644 --- a/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx +++ b/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx @@ -79,10 +79,10 @@ import { MobileSideContentProps } from '../../commons/mobileWorkspace/mobileSide import MobileWorkspace, { MobileWorkspaceProps } from '../../commons/mobileWorkspace/MobileWorkspace'; -import SideContentMarkdownEditor from '../../commons/sideContent/githubAssessments/SideContentMarkdownEditor'; -import SideContentMissionEditor from '../../commons/sideContent/githubAssessments/SideContentMissionEditor'; -import SideContentTaskEditor from '../../commons/sideContent/githubAssessments/SideContentTaskEditor'; -import SideContentTestcaseEditor from '../../commons/sideContent/githubAssessments/SideContentTestcaseEditor'; +import SideContentMarkdownEditor from '../../commons/sideContent/content/githubAssessments/SideContentMarkdownEditor'; +import SideContentMissionEditor from '../../commons/sideContent/content/githubAssessments/SideContentMissionEditor'; +import SideContentTaskEditor from '../../commons/sideContent/content/githubAssessments/SideContentTaskEditor'; +import SideContentTestcaseEditor from '../../commons/sideContent/content/githubAssessments/SideContentTestcaseEditor'; import { SideContentProps } from '../../commons/sideContent/SideContent'; import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; import Constants from '../../commons/utils/Constants'; diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx index b60367ee7f..72ad9a726a 100644 --- a/src/pages/playground/PlaygroundTabs.tsx +++ b/src/pages/playground/PlaygroundTabs.tsx @@ -1,15 +1,15 @@ import { IconNames } from '@blueprintjs/icons'; import { isStepperOutput } from 'js-slang/dist/stepper/stepper'; -import SideContentRemoteExecution from 'src/commons/sideContent/remoteExecution/SideContentRemoteExecution'; -import SideContentEnvVisualizer from 'src/commons/sideContent/SideContentEnvVisualizer'; -import SideContentSubstVisualizer from 'src/commons/sideContent/SideContentSubstVisualizer'; +import SideContentRemoteExecution from 'src/commons/sideContent/content/remoteExecution/SideContentRemoteExecution'; +import SideContentDataVisualizer from 'src/commons/sideContent/content/SideContentDataVisualizer'; +import SideContentEnvVisualizer from 'src/commons/sideContent/content/SideContentEnvVisualizer'; +import SideContentHtmlDisplay from 'src/commons/sideContent/content/SideContentHtmlDisplay'; +import SideContentSubstVisualizer from 'src/commons/sideContent/content/SideContentSubstVisualizer'; +import { SideContentTab, SideContentType } from 'src/commons/sideContent/SideContentTypes'; import { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; import { InterpreterOutput, ResultOutput } from '../../commons/application/ApplicationTypes'; import Markdown from '../../commons/Markdown'; -import SideContentDataVisualizer from '../../commons/sideContent/SideContentDataVisualizer'; -import SideContentHtmlDisplay from '../../commons/sideContent/SideContentHtmlDisplay'; -import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; export const mobileOnlyTabIds: readonly SideContentType[] = [ SideContentType.mobileEditor, diff --git a/src/pages/sourcecast/Sourcecast.tsx b/src/pages/sourcecast/Sourcecast.tsx index 598b26f7e1..88d65a4348 100644 --- a/src/pages/sourcecast/Sourcecast.tsx +++ b/src/pages/sourcecast/Sourcecast.tsx @@ -55,8 +55,8 @@ import { import MobileWorkspace, { MobileWorkspaceProps } from '../../commons/mobileWorkspace/MobileWorkspace'; -import SideContentDataVisualizer from '../../commons/sideContent/SideContentDataVisualizer'; -import SideContentEnvVisualizer from '../../commons/sideContent/SideContentEnvVisualizer'; +import SideContentDataVisualizer from '../../commons/sideContent/content/SideContentDataVisualizer'; +import SideContentEnvVisualizer from '../../commons/sideContent/content/SideContentEnvVisualizer'; import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; import SourceRecorderControlBar, { SourceRecorderControlBarProps From caf2d174ea785778c6ccc7664d5d264dd0bc34e7 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Tue, 12 Sep 2023 03:09:43 +0800 Subject: [PATCH 02/13] Update paths for tests --- .../sideContent/__tests__/SideContentContestLeaderboard.tsx | 2 +- src/commons/sideContent/__tests__/SideContentContestVoting.tsx | 2 +- src/commons/sideContent/__tests__/SideContentEnvVisualizer.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commons/sideContent/__tests__/SideContentContestLeaderboard.tsx b/src/commons/sideContent/__tests__/SideContentContestLeaderboard.tsx index 50ec9f4f76..f9512be001 100644 --- a/src/commons/sideContent/__tests__/SideContentContestLeaderboard.tsx +++ b/src/commons/sideContent/__tests__/SideContentContestLeaderboard.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { shallowRender } from 'src/commons/utils/TestUtils'; -import SideContentContestLeaderboard from '../SideContentContestLeaderboard'; +import SideContentContestLeaderboard from '../content/SideContentContestLeaderboard'; const mockLeaderboardEntries = [ { diff --git a/src/commons/sideContent/__tests__/SideContentContestVoting.tsx b/src/commons/sideContent/__tests__/SideContentContestVoting.tsx index 681ec48c5a..9c8b267cd1 100644 --- a/src/commons/sideContent/__tests__/SideContentContestVoting.tsx +++ b/src/commons/sideContent/__tests__/SideContentContestVoting.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { renderTreeJson } from 'src/commons/utils/TestUtils'; -import SideContentContestVotingContainer from '../SideContentContestVotingContainer'; +import SideContentContestVotingContainer from '../content/SideContentContestVotingContainer'; const mockContestEntries = [ { diff --git a/src/commons/sideContent/__tests__/SideContentEnvVisualizer.tsx b/src/commons/sideContent/__tests__/SideContentEnvVisualizer.tsx index b83715dbd1..7637a28087 100644 --- a/src/commons/sideContent/__tests__/SideContentEnvVisualizer.tsx +++ b/src/commons/sideContent/__tests__/SideContentEnvVisualizer.tsx @@ -6,7 +6,7 @@ import { renderTreeJson } from 'src/commons/utils/TestUtils'; import { mockContext } from '../../mocks/ContextMocks'; import { visualizeEnv } from '../../utils/JsSlangHelper'; -import SideContentEnvVisualizer from '../SideContentEnvVisualizer'; +import SideContentEnvVisualizer from '../content/SideContentEnvVisualizer'; const mockStore = mockInitialStore(); const element = ( From 17575581e26415382ca822cb3eb7ef01e6649635 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Tue, 12 Sep 2023 03:09:58 +0800 Subject: [PATCH 03/13] Add redux toolkit --- package.json | 1 + yarn.lock | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/package.json b/package.json index 8d443213af..7151aaccac 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@blueprintjs/popover2": "^1.14.11", "@blueprintjs/select": "^4.9.24", "@octokit/rest": "^19.0.11", + "@reduxjs/toolkit": "^1.9.5", "@sentry/browser": "^7.57.0", "@sourceacademy/sharedb-ace": "^2.0.2", "@sourceacademy/sling-client": "^0.1.0", diff --git a/yarn.lock b/yarn.lock index f2c654444d..47ca68c818 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1947,6 +1947,16 @@ resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.2.1.tgz#9403f51c17cae37edf870c6bc0c81c1ece5ccef8" integrity sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA== +"@reduxjs/toolkit@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" + integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ== + dependencies: + immer "^9.0.21" + redux "^4.2.1" + redux-thunk "^2.4.2" + reselect "^4.1.8" + "@remix-run/router@1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498" @@ -7143,6 +7153,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immer@^9.0.21: + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + immer@^9.0.7: version "9.0.19" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b" @@ -11206,6 +11221,11 @@ redux-saga@^1.2.3: dependencies: "@redux-saga/core" "^1.2.3" +redux-thunk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" + integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== + redux@^4.0.0, redux@^4.0.4, redux@^4.0.5: version "4.1.2" resolved "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz" @@ -11385,6 +11405,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +reselect@^4.1.8: + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" From 9c96ff1eac4a9e1d07581cac66f200db1c829977 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 18 Sep 2023 18:28:42 +0800 Subject: [PATCH 04/13] First test --- src/commons/redux/ApplicationRedux.ts | 32 +++ src/commons/redux/ReplRedux.ts | 123 ++++++++++ src/commons/redux/SafeEffects.ts | 0 src/commons/redux/SideContentRedux.ts | 52 ++++ src/commons/redux/StoriesRedux.ts | 92 +++++++ .../redux/workspace/AllWorkspacesRedux.ts | 84 +++++++ src/commons/redux/workspace/EditorRedux.ts | 224 ++++++++++++++++++ src/commons/redux/workspace/WorkspaceRedux.ts | 146 ++++++++++++ .../workspace/playground/PlaygroundBase.ts | 73 ++++++ .../workspace/playground/PlaygroundRedux.ts | 180 ++++++++++++++ src/commons/sagas/SideContentSaga.ts | 12 + src/commons/sideContent/SideContentHelper.ts | 2 + .../sideContent/SideContentProvider.tsx | 26 ++ 13 files changed, 1046 insertions(+) create mode 100644 src/commons/redux/ApplicationRedux.ts create mode 100644 src/commons/redux/ReplRedux.ts create mode 100644 src/commons/redux/SafeEffects.ts create mode 100644 src/commons/redux/SideContentRedux.ts create mode 100644 src/commons/redux/StoriesRedux.ts create mode 100644 src/commons/redux/workspace/AllWorkspacesRedux.ts create mode 100644 src/commons/redux/workspace/EditorRedux.ts create mode 100644 src/commons/redux/workspace/WorkspaceRedux.ts create mode 100644 src/commons/redux/workspace/playground/PlaygroundBase.ts create mode 100644 src/commons/redux/workspace/playground/PlaygroundRedux.ts create mode 100644 src/commons/sagas/SideContentSaga.ts create mode 100644 src/commons/sideContent/SideContentProvider.tsx diff --git a/src/commons/redux/ApplicationRedux.ts b/src/commons/redux/ApplicationRedux.ts new file mode 100644 index 0000000000..75efe70582 --- /dev/null +++ b/src/commons/redux/ApplicationRedux.ts @@ -0,0 +1,32 @@ +import { createSlice,PayloadAction } from "@reduxjs/toolkit" +import { setModulesStaticURL } from "js-slang/dist/modules/moduleLoader" +import { SagaIterator } from "redux-saga" +import { call } from "redux-saga/effects" + +import { safeTakeEvery } from "../sagas/SafeEffects" +import Constants from "../utils/Constants" + +export type ApplicationState = { + readonly modulesBackend: string +} + +export const defaultApplication: ApplicationState = { + modulesBackend: Constants.moduleBackendUrl +} + +export const { actions: applicationActions, reducer: applicationReducer } = createSlice({ + name: 'application', + initialState: defaultApplication, + reducers: { + changeModuleBackend(state, { payload }: PayloadAction) { + state.modulesBackend = payload + } + } +}) + +export function* ApplicationSaga(): SagaIterator { + yield safeTakeEvery(applicationActions.changeModuleBackend, function* ({ payload }): SagaIterator { + yield call(setModulesStaticURL, payload) + yield call(console.log, `Using module backend: ${payload}`) + }) +} diff --git a/src/commons/redux/ReplRedux.ts b/src/commons/redux/ReplRedux.ts new file mode 100644 index 0000000000..b539f69191 --- /dev/null +++ b/src/commons/redux/ReplRedux.ts @@ -0,0 +1,123 @@ +import { createSlice,PayloadAction } from "@reduxjs/toolkit"; +import { SourceError, Value } from "js-slang/dist/types"; +import { stringify } from "js-slang/dist/utils/stringify"; + +import { CodeOutput, InterpreterOutput } from "../application/ApplicationTypes" +import Constants from "../utils/Constants"; + +export type ReplState = { + readonly output: InterpreterOutput[] + readonly replHistory: { + readonly browseIndex: null | number; // [0, 49] if browsing, else null + readonly records: string[]; + readonly originalValue: string; + } + readonly replValue: string +} + +export const defaultRepl: ReplState = { + output: [], + replHistory: { + browseIndex: null, + records: [], + originalValue: '' + }, + replValue: '' +} + +export const { actions: replActions, reducer: replReducer } = createSlice({ + name: 'repl', + initialState: defaultRepl, + reducers: { + browseReplHistoryDown(state) { + if (state.replHistory.browseIndex === null) { + // Not yet started browsing history, nothing to do + return; + } else if (state.replHistory.browseIndex !== 0) { + // Browsing history, and still have earlier records to show + const newIndex = state.replHistory.browseIndex - 1; + state.replValue = state.replHistory.records[newIndex]; + state.replHistory.browseIndex = newIndex + } else { + // Browsing history, no earlier records to show; return replValue to + // the last value when user started browsing + state.replHistory.browseIndex = null + state.replValue = state.replHistory.originalValue; + } + }, + browseReplHistoryUp(state) { + const lastRecords = state.replHistory.records; + const lastIndex = state.replHistory.browseIndex; + if ( + lastRecords.length === 0 || + (lastIndex !== null && lastRecords[lastIndex + 1] === undefined) + ) { + // There is no more later history to show + return; + } else if (lastIndex === null) { + // Not yet started browsing, initialise the index & array + state.replHistory.browseIndex = 0; + state.replHistory.originalValue = state.replValue; + } else { + // Browsing history, and still have later history to show + const newIndex = lastIndex + 1; + state.replValue = lastRecords[newIndex]; + state.replHistory.browseIndex = newIndex + } + }, + clearReplInput(state) { state.replValue = '' }, + clearReplOutput(state) { state.output = [] }, + clearReplOutputLast(state) { state.output.pop() }, + evalInterpreterError(state, { payload }: PayloadAction) { + const lastOutput = state.output[state.output.length - 1] + state.output.push({ + type: 'errors', + errors: payload, + consoleLogs: lastOutput !== undefined && lastOutput.type === 'running' ? lastOutput.consoleLogs : [] + }) + }, + evalInterpreterSuccess(state, { payload }: PayloadAction) { + const lastOutput = state.output[state.output.length - 1] + state.output.push({ + type: 'result', + value: stringify(payload), + consoleLogs: lastOutput !== undefined && lastOutput.type === 'running' ? lastOutput.consoleLogs : [] + }) + }, + handleConsoleLog(state, { payload }: PayloadAction) { + /* Possible cases: + * (1) state[workspaceLocation].output === [], i.e. state[workspaceLocation].output[-1] === undefined + * (2) state[workspaceLocation].output[-1] is not RunningOutput + * (3) state[workspaceLocation].output[-1] is RunningOutput */ + const lastOutput = state.output[state.output.length - 1]; + if (lastOutput === undefined || lastOutput.type !== 'running') { + // New block of output. + state.output.push({ + type: 'running', + consoleLogs: payload, + }); + } else { + const updatedLastOutput = { + type: lastOutput.type, + consoleLogs: lastOutput.consoleLogs.concat(payload) + }; + state.output[state.output.length - 1] = updatedLastOutput + } + }, + sendReplInputToOutput(state, { payload }: PayloadAction) { + // CodeOutput properties exist in parallel with workspaceLocation + state.output.push(payload) + + if (payload.value !== '') { + state.replHistory.records.unshift(payload.value) + } else { + if (state.replHistory.records.length === Constants.maxBrowseIndex) { + state.replHistory.records.pop() + } + } + }, + updateReplValue(state, { payload }: PayloadAction) { + state.replValue = payload + } + } +}) diff --git a/src/commons/redux/SafeEffects.ts b/src/commons/redux/SafeEffects.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/commons/redux/SideContentRedux.ts b/src/commons/redux/SideContentRedux.ts new file mode 100644 index 0000000000..6b917b91f2 --- /dev/null +++ b/src/commons/redux/SideContentRedux.ts @@ -0,0 +1,52 @@ +import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit' + +import { getDynamicTabs, getTabId } from '../sideContent/SideContentHelper' +import { SideContentTab, SideContentType } from '../sideContent/SideContentTypes' +import { DebuggerContext, WorkspaceLocation } from '../workspace/WorkspaceTypes' + +export type SideContentLocation = Exclude | `stories.${string}` + +export type SideContentState = { + dynamicTabs: SideContentTab[] + alerts: string[] + selectedTabId?: SideContentType + height?: number +} + +export const defaultSideContent: SideContentState = { + dynamicTabs: [], + alerts: [] +} + +const { actions, reducer } = createSlice({ + name: 'sideContent', + initialState: defaultSideContent, + reducers: { + changeSideContentHeight(state, { payload }: PayloadAction) { + state.height = payload + }, + endAlertSideContentHeight(state, { payload: newId }: PayloadAction) { + if (newId === state.selectedTabId) return + + state.alerts.push(newId) + }, + notifyProgramEvaluated(state, { payload }: PayloadAction) { + const dynamicTabs = getDynamicTabs(payload) + state.alerts = dynamicTabs.map(getTabId).filter(id => id !== state.selectedTabId) + state.dynamicTabs = dynamicTabs + }, + visitSideContent(state, { payload: newId }: PayloadAction) { + if (newId === state.selectedTabId) return + + state.alerts = state.alerts.filter(id => id !== newId) + state.selectedTabId = newId + } + } +}) + +export const sideContentActions = { + ...actions, + beginAlertSideContent: createAction('sideContent/beginAlertSideContent', (newId: SideContentType) => ({ payload: newId })) +} + +export { reducer as sideContentReducer } diff --git a/src/commons/redux/StoriesRedux.ts b/src/commons/redux/StoriesRedux.ts new file mode 100644 index 0000000000..6eaad97ece --- /dev/null +++ b/src/commons/redux/StoriesRedux.ts @@ -0,0 +1,92 @@ +import { createSlice,PayloadAction } from "@reduxjs/toolkit"; +import { Chapter, Variant } from "js-slang/dist/types"; +import { StoryData, StoryListView } from "src/features/stories/StoriesTypes"; + +import { StoriesRole } from "../application/ApplicationTypes"; +import Constants from "../utils/Constants"; +import { createContext } from "../utils/JsSlangHelper"; +import { getDefaultPlaygroundState,PlaygroundWorkspaceState } from "./workspace/playground/PlaygroundBase"; + +export type StoriesAuthState = { + readonly userId?: number; + readonly userName?: string; + readonly groupId?: number; + readonly groupName?: string; + readonly role?: StoriesRole; +}; + +export type StoriesEnvState = PlaygroundWorkspaceState + +export type StoriesState = { + readonly storyList: StoryListView[]; + readonly currentStoryId: number | null; + readonly currentStory: StoryData | null; + readonly envs: { [key: string]: StoriesEnvState }; +} & StoriesAuthState; + +export const defaultStories: StoriesState = { + storyList: [], + currentStory: null, + currentStoryId: null, + envs: {} +} + +export const getDefaultStoriesEnv = ( + env: string, + chapter: Chapter = Constants.defaultSourceChapter, + variant: Variant = Constants.defaultSourceVariant +): StoriesEnvState => ({ + ...getDefaultPlaygroundState(), + context: createContext( + chapter, + [], + env, + variant + ) +}) + +export const { actions: storiesActions, reducer: storiesReducer } = createSlice({ + name: 'stories', + initialState: defaultStories, + reducers: { + addStoryEnv: { + prepare: (env: string, chapter: Chapter, variant: Variant) => ({ payload: { env, chapter, variant }}), + reducer(state, { payload }: PayloadAction<{ env: string, chapter: Chapter, variant: Variant }>) { + state.envs[payload.env] = getDefaultStoriesEnv(payload.env, payload.chapter, payload.variant) + } + }, + clearStoryEnv(state, { payload: env }: PayloadAction) { + if (env === undefined) { + state.envs = {} + } else { + const { chapter, variant } = state.envs[env].context + state.envs[env] = getDefaultStoriesEnv(env, chapter, variant) + } + }, + // TODO: Handle logout + setCurrentStory(state, { payload }: PayloadAction) { + state.currentStory = payload + }, + setCurrentStoryId(state, { payload }: PayloadAction) { + state.currentStoryId = payload + }, + setCurrentStoriesGroup: { + prepare: (id?: number, name?: string, role?: StoriesRole) => ({ payload: { id, name, role }}), + reducer(state, { payload }: PayloadAction<{ id?: number, name?: string, role?: StoriesRole }>) { + state.groupId = payload.id + state.groupName = payload.name + state.role = payload.role + } + }, + setCurrentStoriesUser: { + prepare: (id?: number, name?: string) => ({ payload: { id, name }}), + reducer(state, { payload }: PayloadAction<{ id?: number, name?: string }>) { + state.userId = payload.id + state.userName = payload.name + } + }, + updateStoriesList(state, { payload }: PayloadAction) { + state.storyList = payload + } + } +}) diff --git a/src/commons/redux/workspace/AllWorkspacesRedux.ts b/src/commons/redux/workspace/AllWorkspacesRedux.ts new file mode 100644 index 0000000000..38fcc1b7d9 --- /dev/null +++ b/src/commons/redux/workspace/AllWorkspacesRedux.ts @@ -0,0 +1,84 @@ +import { ActionCreatorWithPreparedPayload, PayloadAction, PayloadActionCreator } from "@reduxjs/toolkit"; +import { SourceActionType } from "src/commons/utils/ActionsHelper"; +import { StoriesState } from "src/features/stories/StoriesTypes"; + +import { SideContentLocation } from "../SideContentRedux"; +import { getDefaultStoriesEnv } from "../StoriesRedux"; +import { basePlaygroundReducer } from "./playground/PlaygroundBase"; +import { playgroundReducer,PlaygroundState } from "./playground/PlaygroundRedux"; +import { createWorkspaceSlice, getDefaultWorkspaceState } from "./WorkspaceRedux"; + +const { actions } = createWorkspaceSlice('sicp', getDefaultWorkspaceState([]), { + testAction(state) {} +}) + +type WorkspaceManagerState = { + playground: PlaygroundState + stories: StoriesState +} + +type AllWorkspaceActions = { + [K in keyof typeof actions]: ActionCreatorWithPreparedPayload< + [location: SideContentLocation, ...Parameters], + { payload: ReturnType['payload'], location: SideContentLocation }, + (typeof actions)[K]['type'] + > +} + +export const allWorkspaceActions = Object.entries(actions).reduce((res, [name, creator]) => ({ + ...res, + [name]: (location: SideContentLocation, ...args: any) => { + // @ts-ignore + const action = (creator as PayloadActionCreator>)(...args) + return { + ...action, + payload: { + payload: action.payload, + location + } + } + } +}), {} as AllWorkspaceActions) + +const allWorkspaceReducers = { + playground: playgroundReducer, +} + +export function allWorkspacesReducer(state: WorkspaceManagerState, action: SourceActionType) { + let workspaceLocation: SideContentLocation + if ((action as any).location) { + workspaceLocation = (action as any).location + } else { + workspaceLocation = 'assessment' + } + + switch(action.type) { + default: { + const newAction = { + ...action, + payload: (action as any).payload + } + + if (workspaceLocation.startsWith('stories')) { + const [, storyEnv] = workspaceLocation.split('.') + const storyReducer = basePlaygroundReducer(getDefaultStoriesEnv(storyEnv)) + + return { + ...state, + stories: { + ...state.stories, + envs: { + ...state.stories.envs, + [storyEnv]: storyReducer(state.stories[storyEnv], action) + } + } + } + } + + return { + ...state, + [workspaceLocation]: allWorkspaceReducers[workspaceLocation](state[workspaceLocation], newAction) + } + } + } +} diff --git a/src/commons/redux/workspace/EditorRedux.ts b/src/commons/redux/workspace/EditorRedux.ts new file mode 100644 index 0000000000..0a7292fbae --- /dev/null +++ b/src/commons/redux/workspace/EditorRedux.ts @@ -0,0 +1,224 @@ +import { createSlice,PayloadAction } from "@reduxjs/toolkit" +import { HighlightedLines, Position } from "src/commons/editor/EditorTypes" +import { EditorTabState } from "src/commons/workspace/WorkspaceTypes" + +export type EditorState = { + readonly activeEditorTabIndex: number | null + readonly editorSessionId: string + readonly editorTabs: EditorTabState[] + + readonly isEditorAutorun: boolean + readonly isEditorReadonly: boolean +} + +export const getDefaultEditorState = (defaultTabs: EditorTabState[] = []): EditorState => ({ + activeEditorTabIndex: 0, + editorSessionId: '', + editorTabs: defaultTabs, + isEditorAutorun: false, + isEditorReadonly: false +}) + +export const getEditorSlice = (defaultTabs: EditorTabState[] = []) => createSlice({ + name: 'editor', + initialState: getDefaultEditorState(defaultTabs), + reducers: { + addEditorTab: { + prepare: (filePath: string, editorValue: string) => ({ payload: { filePath, editorValue }}), + reducer(state, { payload }: PayloadAction<{ filePath: string, editorValue: string}>) { + const { filePath, editorValue } = payload; + + const editorTabs = state.editorTabs; + const openedEditorTabIndex = editorTabs.findIndex( + (editorTab: EditorTabState) => editorTab.filePath === filePath + ); + const fileIsAlreadyOpen = openedEditorTabIndex !== -1; + if (fileIsAlreadyOpen) { + // If the file is already opened just swap to the tab + state.activeEditorTabIndex = openedEditorTabIndex + return + } + + state.editorTabs.push({ + filePath, + value: editorValue, + highlightedLines: [], + breakpoints: [] + }) + + // Check if this works properly + state.activeEditorTabIndex = state.editorTabs.length + 1 + } + }, + moveCursor: { + prepare: (editorTabIndex: number, newCursorPosition: Position) => ({ payload: { editorTabIndex, newCursorPosition }}), + reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newCursorPosition: Position }>) { + const { editorTabIndex, newCursorPosition } = payload; + if (editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + + state.editorTabs[editorTabIndex].newCursorPosition = newCursorPosition + } + }, + removeEditorTab(state, { payload: editorTabIndex }: PayloadAction) { + if (editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + + const activeEditorTabIndex = state.activeEditorTabIndex; + const newActiveEditorTabIndex = getNextActiveEditorTabIndexAfterTabRemoval( + activeEditorTabIndex, + editorTabIndex, + state.editorTabs.length - 1 + ); + + state.activeEditorTabIndex = newActiveEditorTabIndex + state.editorTabs.splice(editorTabIndex, 1) + }, + setEditorSessionId(state, { payload }: PayloadAction) { + state.editorSessionId = payload + }, + setIsEditorAutorun(state, { payload }: PayloadAction) { + state.isEditorAutorun = payload + }, + setIsEditorReadonly(state, { payload }: PayloadAction) { + state.isEditorReadonly = payload + }, + shiftEditorTab: { + prepare: (previousIndex: number, newIndex: number) => ({ payload: { previousIndex, newIndex }}), + reducer(state, action: PayloadAction<{ previousIndex: number, newIndex: number}>) { + const { previousIndex, newIndex } = action.payload; + if (previousIndex < 0) { + throw new Error('Previous editor tab index must be non-negative!'); + } + if (previousIndex >= state.editorTabs.length) { + throw new Error('Previous editor tab index must have a corresponding editor tab!'); + } + if (newIndex < 0) { + throw new Error('New editor tab index must be non-negative!'); + } + if (newIndex >= state.editorTabs.length) { + throw new Error('New editor tab index must have a corresponding editor tab!'); + } + state.activeEditorTabIndex = + state.activeEditorTabIndex === previousIndex + ? newIndex + : state.activeEditorTabIndex; + const editorTabs = state.editorTabs; + const shiftedEditorTab = editorTabs[previousIndex]; + const filteredEditorTabs = editorTabs.filter( + (editorTab: EditorTabState, index: number) => index !== previousIndex + ); + state.editorTabs = [ + ...filteredEditorTabs.slice(0, newIndex), + shiftedEditorTab, + ...filteredEditorTabs.slice(newIndex) + ]; + } + }, + updateActiveEditorTab(state, { payload: activeEditorTabOptions }: PayloadAction | undefined>) { + const activeEditorTabIndex = state.activeEditorTabIndex; + // Do not modify the workspace state if there is no active editor tab. + if (activeEditorTabIndex === null) return + + state.editorTabs[activeEditorTabIndex] = { + ...state.editorTabs[activeEditorTabIndex], + ...activeEditorTabOptions + } + }, + updateActiveEditorTabIndex(state, { payload: activeEditorTabIndex }: PayloadAction) { + if (activeEditorTabIndex !== null) { + if (activeEditorTabIndex < 0) { + throw new Error('Active editor tab index must be non-negative!'); + } + if (activeEditorTabIndex >= state.editorTabs.length) { + throw new Error('Active editor tab index must have a corresponding editor tab!'); + } + } + state.activeEditorTabIndex = activeEditorTabIndex + }, + updateEditorBreakpoints: { + prepare: (editorTabIndex: number, newBreakpoints: string[]) => ({ payload: { editorTabIndex, newBreakpoints }}), + reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newBreakpoints: string[] }>) { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + state.editorTabs[payload.editorTabIndex].breakpoints = payload.newBreakpoints + } + }, + updateEditorHighlightedLines: { + prepare: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }}), + reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newHighlightedLines: HighlightedLines[] }>) { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines + } + }, + updateEditorHighlightedLinesAgenda: { + prepare: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }}), + reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newHighlightedLines: HighlightedLines[] }>) { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines + } + }, + updateEditorValue: { + prepare: (editorTabIndex: number, newEditorValue: string) => ({ payload: { editorTabIndex, newEditorValue }}), + reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newEditorValue: string}>) { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + state.editorTabs[payload.editorTabIndex].value = payload.newEditorValue + } + }, + } +}) + +const getNextActiveEditorTabIndexAfterTabRemoval = ( + activeEditorTabIndex: number | null, + removedEditorTabIndex: number, + newEditorTabsLength: number +) => { + return activeEditorTabIndex !== removedEditorTabIndex + ? // If the active editor tab is not the one that is removed, + // the active editor tab remains the same if its index is + // less than the removed editor tab index or null. + activeEditorTabIndex === null || activeEditorTabIndex < removedEditorTabIndex + ? activeEditorTabIndex + : // Otherwise, the active editor tab index needs to have 1 + // subtracted because every tab to the right of the editor + // tab being removed has their index decremented by 1. + activeEditorTabIndex - 1 + : newEditorTabsLength === 0 + ? // If there are no editor tabs after removal, there cannot + // be an active editor tab. + null + : removedEditorTabIndex === 0 + ? // If the removed editor tab is the leftmost tab, the active + // editor tab will be the new leftmost tab. + 0 + : // Otherwise, the active editor tab will be the tab to the + // left of the removed tab. + removedEditorTabIndex - 1; +}; diff --git a/src/commons/redux/workspace/WorkspaceRedux.ts b/src/commons/redux/workspace/WorkspaceRedux.ts new file mode 100644 index 0000000000..f981dbc366 --- /dev/null +++ b/src/commons/redux/workspace/WorkspaceRedux.ts @@ -0,0 +1,146 @@ +import { combineReducers, createSlice, Draft, PayloadAction, SliceCaseReducers } from "@reduxjs/toolkit"; +import { Context } from "js-slang/dist/types"; +import { InterpreterOutput } from "src/commons/application/ApplicationTypes"; +import Constants from "src/commons/utils/Constants"; +import { createContext } from "src/commons/utils/JsSlangHelper"; +import { DebuggerContext, EditorTabState } from "src/commons/workspace/WorkspaceTypes"; + +import { defaultRepl,replActions,replReducer,ReplState } from "../ReplRedux"; +import { defaultSideContent, sideContentActions, sideContentReducer, SideContentState } from "../SideContentRedux"; +import { EditorState, getDefaultEditorState, getEditorSlice } from "./EditorRedux"; + +export type WorkspaceState = { + readonly context: Context; + readonly debuggerContext: DebuggerContext; + + readonly editorState: EditorState + readonly enableDebugging: boolean; + readonly execTime: number; + + readonly globals: Array<[string, any]>; + + readonly isDebugging: boolean; + readonly isEditorAutorun: boolean; + readonly isEditorReadonly: boolean; + readonly isFolderModeEnabled: boolean; + readonly isRunning: boolean; + + readonly output: InterpreterOutput[]; + + readonly programPrependValue: string; + readonly programPostpendValue: string; + readonly repl: ReplState; + readonly sideContent: SideContentState + // readonly sharedbConnected: boolean; +} + +export const getDefaultWorkspaceState = (initialTabs: EditorTabState[] = []): WorkspaceState => ({ + context: createContext( + Constants.defaultSourceChapter, + [], + {}, + Constants.defaultSourceVariant + ), + debuggerContext: {} as DebuggerContext, + editorState: getDefaultEditorState(initialTabs), + enableDebugging: true, + execTime: 1000, + isDebugging: false, + isEditorAutorun: false, + isEditorReadonly: false, + isFolderModeEnabled: false, + isRunning: false, + globals: [], + output: [], + repl: defaultRepl, + programPostpendValue: '', + programPrependValue: '', + sideContent: defaultSideContent, +}) + +const workspaceReducers = { + debugReset(state: Draft) { + state.isDebugging = false; + state.isRunning = false; + }, + debugResume(state: Draft) { + state.isDebugging = false; + state.isRunning = true; + }, + endClearContext(state: Draft) { + // TODO Investigate + }, + endDebugPause(state: Draft) { + state.isDebugging = true; + state.isRunning = false; + }, + endInterruptExecution(state: Draft) { + // same as debug reset + state.isDebugging = false; + state.isRunning = false; + }, + evalEditor(state: Draft) { + state.isDebugging = false; + state.isRunning = true; + }, + evalRepl(state: Draft) { + state.isRunning = true; + }, + setFolderMode(state: Draft, { payload }: PayloadAction) { + state.isFolderModeEnabled = payload; + }, +} as const + +type BaseWorkspaceReducers = typeof workspaceReducers + +export const createWorkspaceSlice = < + TState extends WorkspaceState, + TReducers extends SliceCaseReducers, + TName extends string = string +>( + name: TName, + initialState: TState, + reducers: TReducers, +) => { + const { actions: editorActions, reducer: editorReducer } = getEditorSlice(initialState.editorState.editorTabs) + + const subReducer = combineReducers({ + editorState: editorReducer, + sideContent: sideContentReducer, + repl: replReducer + }) + + const { actions, reducer } = createSlice({ + name, + initialState: initialState, + reducers: { + ...workspaceReducers, + ...reducers, + } as any, + extraReducers: builder => { + builder.addCase(replActions.evalInterpreterError, state => { + state.isDebugging = false; + state.isRunning = false; + }); + + builder.addCase(replActions.evalInterpreterSuccess, state => { + state.isRunning = false; + }); + + builder.addCase(sideContentActions.notifyProgramEvaluated, (state, { payload }) => { + state.debuggerContext = payload; + }); + + builder.addDefaultCase((state, action) => { + subReducer(state, action) + }) + } + }); + + return { reducer, actions: { + ...editorActions, + ...sideContentActions, + ...replActions, + ...actions, + }} +} diff --git a/src/commons/redux/workspace/playground/PlaygroundBase.ts b/src/commons/redux/workspace/playground/PlaygroundBase.ts new file mode 100644 index 0000000000..d457d39972 --- /dev/null +++ b/src/commons/redux/workspace/playground/PlaygroundBase.ts @@ -0,0 +1,73 @@ +import { createReducer, Draft, PayloadAction, SliceCaseReducers } from "@reduxjs/toolkit" +import { EditorTabState } from "src/commons/workspace/WorkspaceTypes" + +import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "../WorkspaceRedux" + +type PlaygroundAttr = { + readonly breakpointSteps: number[] + readonly envSteps: number + readonly envStepsTotal: number + readonly stepLimit: number + readonly updateEnv: boolean + readonly usingEnv: boolean + + readonly usingSubst: boolean +} + +export type PlaygroundWorkspaceState = PlaygroundAttr & WorkspaceState + +export const getDefaultPlaygroundState = (initialTabs: EditorTabState[] = []): PlaygroundWorkspaceState => ({ + ...getDefaultWorkspaceState(initialTabs), + breakpointSteps: [], + envSteps: -1, + envStepsTotal: 0, + stepLimit: 1000, + updateEnv: true, + usingEnv: false, + usingSubst: false, +}) + +const basePlaygroundReducers = { + changeStepLimit(state: Draft, { payload }: PayloadAction) { + state.stepLimit = payload + }, + toggleUpdateEnv(state: Draft, { payload }: PayloadAction) { + state.updateEnv = payload + }, + toggleUsingEnv(state: Draft, { payload }: PayloadAction) { + state.usingEnv = payload + }, + toggleUsingSubst(state: Draft, { payload }: PayloadAction) { + state.usingSubst = payload + }, + updateBreakpointSteps(state: Draft, { payload }: PayloadAction) { + state.breakpointSteps = payload + }, + updateEnvSteps(state: Draft, { payload }: PayloadAction) { + state.envSteps = payload + }, + updateEnvStepsTotal(state: Draft, { payload }: PayloadAction) { + state.envStepsTotal = payload + } +} as const + +export const basePlaygroundReducer = (initialState: T) => createReducer(initialState, basePlaygroundReducers) + +type PlaygroundBaseReducers = typeof basePlaygroundReducers + +export const createPlaygroundSlice = < + TState extends PlaygroundWorkspaceState, + TReducers extends SliceCaseReducers, + TName extends string = string +>( + name: TName, + initialState: TState, + reducers: TReducers, +) => createWorkspaceSlice( + name, + initialState, + { + ...basePlaygroundReducers, + ...reducers, + } +) diff --git a/src/commons/redux/workspace/playground/PlaygroundRedux.ts b/src/commons/redux/workspace/playground/PlaygroundRedux.ts new file mode 100644 index 0000000000..55c7b1e393 --- /dev/null +++ b/src/commons/redux/workspace/playground/PlaygroundRedux.ts @@ -0,0 +1,180 @@ +import { createAction,PayloadAction } from "@reduxjs/toolkit"; +import { FSModule } from "browserfs/dist/node/core/FS"; +import { Chapter, Variant } from "js-slang/dist/types"; +import { compressToEncodedURIComponent } from "lz-string"; +import * as qs from 'query-string'; +import { SagaIterator } from "redux-saga"; +import { call, delay, put, race, select } from "redux-saga/effects"; +import { defaultEditorValue, defaultLanguageConfig, getDefaultFilePath, OverallState, SALanguage } from "src/commons/application/ApplicationTypes"; +import { ExternalLibraryName } from "src/commons/application/types/ExternalTypes"; +import { retrieveFilesInWorkspaceAsRecord } from "src/commons/fileSystem/utils"; +import { safeTakeEvery as takeEvery } from "src/commons/sagas/SafeEffects"; +import Constants from "src/commons/utils/Constants"; +import { showSuccessMessage, showWarningMessage } from "src/commons/utils/notifications/NotificationsHelper"; +import { EditorTabState } from "src/commons/workspace/WorkspaceTypes"; +import { GitHubSaveInfo } from "src/features/github/GitHubTypes"; +import { PersistenceFile } from "src/features/persistence/PersistenceTypes"; + +import { createPlaygroundSlice, getDefaultPlaygroundState, PlaygroundWorkspaceState } from "./PlaygroundBase"; + +export type PlaygroundState = PlaygroundWorkspaceState & { + readonly githubSaveInfo: GitHubSaveInfo + readonly languageConfig: SALanguage + readonly persistenceFile?: PersistenceFile + readonly queryString?: string + readonly shortURL?: string +} + +export const defaultPlayground: PlaygroundState = { + ...getDefaultPlaygroundState([{ + breakpoints: [], + filePath: getDefaultFilePath('playground'), + highlightedLines: [], + value: defaultEditorValue + }]), + githubSaveInfo: { repoName: '', filePath: '' }, + languageConfig: defaultLanguageConfig +} + +export const { actions: playgroundWorkspaceActions, reducer: playgroundReducer } = createPlaygroundSlice('playground', defaultPlayground, { + changeQueryString(state, { payload }: PayloadAction) { + state.queryString = payload + }, + playgroundUpdateGithubSaveInfo(state, { payload }: PayloadAction) { + state.githubSaveInfo = payload + }, + playgroundUpdatePersistenceFile(state, { payload }: PayloadAction) { + state.persistenceFile = payload + }, + playgroundUpdateLanguageConfig(state, { payload }: PayloadAction) { + state.languageConfig = payload + }, + updateShortURL(state, { payload }: PayloadAction) { + state.shortURL = payload + } +}) + +export const playgroundActions = { + ...playgroundWorkspaceActions, + generateLzString: createAction('playground/generateLzString'), + shortenUrl: createAction('playground/shortenURL', (url: string) => ({ payload: url })) +} + +export function* playgroundSaga(): SagaIterator { + yield takeEvery(playgroundActions.generateLzString, updateQueryString) + + yield takeEvery(playgroundActions.shortenUrl, function* ({ payload: keyword }): SagaIterator { + const queryString = yield select((state: OverallState) => state.playground.queryString); + const errorMsg = 'ERROR'; + + let resp, timeout; + + //we catch and move on if there are errors (plus have a timeout in case) + try { + const { result, hasTimedOut } = yield race({ + result: call(shortenURLRequest, queryString, keyword), + hasTimedOut: delay(10000) + }); + + resp = result; + timeout = hasTimedOut; + } catch (_) {} + + if (!resp || timeout) { + yield put(playgroundWorkspaceActions.updateShortURL(errorMsg)); + return yield call(showWarningMessage, 'Something went wrong trying to create the link.'); + } + + if (resp.status !== 'success' && !resp.shorturl) { + yield put(playgroundActions.updateShortURL(errorMsg)); + return yield call(showWarningMessage, resp.message); + } + + if (resp.status !== 'success') { + yield call(showSuccessMessage, resp.message); + } + yield put(playgroundWorkspaceActions.updateShortURL(Constants.urlShortenerBase + resp.url.keyword)); + }) +} + +function* updateQueryString() { + const isFolderModeEnabled: boolean = yield select( + (state: OverallState) => state.workspaces.playground.isFolderModeEnabled + ); + const fileSystem: FSModule = yield select( + (state: OverallState) => state.fileSystem.inBrowserFileSystem + ); + const files: Record = yield call( + retrieveFilesInWorkspaceAsRecord, + 'playground', + fileSystem + ); + const editorTabs: EditorTabState[] = yield select( + (state: OverallState) => state.workspaces.playground.editorTabs + ); + const editorTabFilePaths = editorTabs + .map((editorTab: EditorTabState) => editorTab.filePath) + .filter((filePath): filePath is string => filePath !== undefined); + const activeEditorTabIndex: number | null = yield select( + (state: OverallState) => state.workspaces.playground.activeEditorTabIndex + ); + const chapter: Chapter = yield select( + (state: OverallState) => state.workspaces.playground.context.chapter + ); + const variant: Variant = yield select( + (state: OverallState) => state.workspaces.playground.context.variant + ); + const external: ExternalLibraryName = yield select( + (state: OverallState) => state.workspaces.playground.externalLibrary + ); + const execTime: number = yield select( + (state: OverallState) => state.workspaces.playground.execTime + ); + const newQueryString = qs.stringify({ + isFolder: isFolderModeEnabled, + files: compressToEncodedURIComponent(qs.stringify(files)), + tabs: editorTabFilePaths.map(compressToEncodedURIComponent), + tabIdx: activeEditorTabIndex, + chap: chapter, + variant, + ext: external, + exec: execTime + }); + yield put(playgroundWorkspaceActions.changeQueryString(newQueryString)); +} + + +/** + * Gets short url from microservice + * @returns {(Response|null)} Response if successful, otherwise null. + */ +export async function shortenURLRequest( + queryString: string, + keyword: string +): Promise { + const url = `${window.location.protocol}//${window.location.host}/playground#${queryString}`; + + const params = { + signature: Constants.urlShortenerSignature, + action: 'shorturl', + format: 'json', + keyword, + url + }; + const fetchOpts: RequestInit = { + method: 'POST', + body: Object.entries(params).reduce((formData, [k, v]) => { + formData.append(k, v!); + return formData; + }, new FormData()) + }; + + const resp = await fetch(`${Constants.urlShortenerBase}yourls-api.php`, fetchOpts); + if (!resp || !resp.ok) { + return null; + } + + const res = await resp.json(); + return res; +} + diff --git a/src/commons/sagas/SideContentSaga.ts b/src/commons/sagas/SideContentSaga.ts new file mode 100644 index 0000000000..0745bd690f --- /dev/null +++ b/src/commons/sagas/SideContentSaga.ts @@ -0,0 +1,12 @@ +import { SagaIterator } from "redux-saga"; +import { put, take } from "redux-saga/effects"; + +import { sideContentActions } from "../sideContent/SideContentRedux"; +import { safeTakeEvery as takeEvery } from "./SafeEffects"; + +export default function* SideContentSaga(): SagaIterator { + yield takeEvery(sideContentActions.beginAlertSideContent, function* ({ payload }: ReturnType) { + yield take(sideContentActions.notifyProgramEvaluated) + yield put(sideContentActions.endAlertSideContent(payload.location, payload.payload)) + }) +} diff --git a/src/commons/sideContent/SideContentHelper.ts b/src/commons/sideContent/SideContentHelper.ts index f37579424a..344c026a3d 100644 --- a/src/commons/sideContent/SideContentHelper.ts +++ b/src/commons/sideContent/SideContentHelper.ts @@ -42,3 +42,5 @@ export const getDynamicTabs = (debuggerContext: DebuggerContext): SideContentTab id: SideContentType.module })); }; + +export const getTabId = (tab: SideContentTab) => tab.id === undefined || tab.id === SideContentType.module ? tab.label : tab.id diff --git a/src/commons/sideContent/SideContentProvider.tsx b/src/commons/sideContent/SideContentProvider.tsx new file mode 100644 index 0000000000..853b921c62 --- /dev/null +++ b/src/commons/sideContent/SideContentProvider.tsx @@ -0,0 +1,26 @@ +import { useSideContent } from "./SideContentHelper" +import { ChangeTabsCallback, SideContentLocation, SideContentTab, SideContentType } from "./SideContentTypes" + + +type SideContentProviderProps = { + location: SideContentLocation + tabs?: { + beforeDynamicTabs: SideContentTab[] + afterDynamicTabs: SideContentTab[] + } + defaultTab?: SideContentType + onChange?: ChangeTabsCallback + children: (allTabs: SideContentTab[], changeTabsCallback: ChangeTabsCallback, alerts: string[], selectedTabId?: SideContentType, height?: number) => JSX.Element +} + +export const SideContentProvider = (props: SideContentProviderProps) => { + const sideContent = useSideContent(props.location, props.defaultTab) + const allTabs = props.tabs ? [ + ...props.tabs.beforeDynamicTabs, + ...sideContent.dynamicTabs, + ...props.tabs.afterDynamicTabs + ] : sideContent.dynamicTabs + + return props.children(allTabs, props.onChange ?? sideContent.setSelectedTab, sideContent.alerts, sideContent.selectedTab, sideContent.height) + +} From b851b5f2229b8e426fbf3e2e3a4f4e70fc687f62 Mon Sep 17 00:00:00 2001 From: Lee Yi Date: Mon, 18 Sep 2023 22:23:59 +0800 Subject: [PATCH 05/13] Update redux --- src/commons/redux/ReplRedux.ts | 197 +++--- src/commons/redux/SafeEffects.ts | 0 src/commons/redux/SideContentRedux.ts | 60 +- src/commons/redux/Sourcereel.ts | 60 ++ .../redux/workspace/AllWorkspacesRedux.ts | 82 ++- src/commons/redux/workspace/EditorRedux.ts | 615 ++++++++++++++---- src/commons/redux/workspace/GradingRedux.ts | 35 + src/commons/redux/workspace/WorkspaceRedux.ts | 148 +++-- .../workspace/playground/PlaygroundBase.ts | 115 +++- .../workspace/playground/PlaygroundRedux.ts | 5 +- 10 files changed, 966 insertions(+), 351 deletions(-) delete mode 100644 src/commons/redux/SafeEffects.ts create mode 100644 src/commons/redux/Sourcereel.ts create mode 100644 src/commons/redux/workspace/GradingRedux.ts diff --git a/src/commons/redux/ReplRedux.ts b/src/commons/redux/ReplRedux.ts index b539f69191..048b39a5f7 100644 --- a/src/commons/redux/ReplRedux.ts +++ b/src/commons/redux/ReplRedux.ts @@ -1,4 +1,4 @@ -import { createSlice,PayloadAction } from "@reduxjs/toolkit"; +import { createAction, createReducer } from "@reduxjs/toolkit"; import { SourceError, Value } from "js-slang/dist/types"; import { stringify } from "js-slang/dist/utils/stringify"; @@ -25,99 +25,112 @@ export const defaultRepl: ReplState = { replValue: '' } -export const { actions: replActions, reducer: replReducer } = createSlice({ - name: 'repl', - initialState: defaultRepl, - reducers: { - browseReplHistoryDown(state) { - if (state.replHistory.browseIndex === null) { - // Not yet started browsing history, nothing to do - return; - } else if (state.replHistory.browseIndex !== 0) { - // Browsing history, and still have earlier records to show - const newIndex = state.replHistory.browseIndex - 1; - state.replValue = state.replHistory.records[newIndex]; - state.replHistory.browseIndex = newIndex - } else { - // Browsing history, no earlier records to show; return replValue to - // the last value when user started browsing - state.replHistory.browseIndex = null - state.replValue = state.replHistory.originalValue; - } - }, - browseReplHistoryUp(state) { - const lastRecords = state.replHistory.records; - const lastIndex = state.replHistory.browseIndex; - if ( - lastRecords.length === 0 || - (lastIndex !== null && lastRecords[lastIndex + 1] === undefined) - ) { - // There is no more later history to show - return; - } else if (lastIndex === null) { - // Not yet started browsing, initialise the index & array - state.replHistory.browseIndex = 0; - state.replHistory.originalValue = state.replValue; - } else { - // Browsing history, and still have later history to show - const newIndex = lastIndex + 1; - state.replValue = lastRecords[newIndex]; - state.replHistory.browseIndex = newIndex - } - }, - clearReplInput(state) { state.replValue = '' }, - clearReplOutput(state) { state.output = [] }, - clearReplOutputLast(state) { state.output.pop() }, - evalInterpreterError(state, { payload }: PayloadAction) { - const lastOutput = state.output[state.output.length - 1] - state.output.push({ - type: 'errors', - errors: payload, - consoleLogs: lastOutput !== undefined && lastOutput.type === 'running' ? lastOutput.consoleLogs : [] - }) - }, - evalInterpreterSuccess(state, { payload }: PayloadAction) { - const lastOutput = state.output[state.output.length - 1] +export const replActions = { + browseReplHistoryDown: createAction('repl/browseReplHistoryDown'), + browseReplHistoryUp: createAction('repl/browseReplHistoryUp'), + clearReplInput: createAction('repl/clearReplInput'), + clearReplOutput: createAction('repl/clearReplOutput'), + clearReplOutputLast: createAction('rep;/clearReplOutputLast'), + evalInterpreterError: createAction('repl/evalInterpreterError', (payload: SourceError[]) => ({ payload })), + evalInterpreterSuccess: createAction('repl/evalInterpreterSuccess', (payload: Value) => ({ payload })), + handleConsoleLog: createAction('repl/handleConsoleLog', (payload: string[]) => ({ payload })), + sendReplInputToOutput: createAction('repl/sendReplInputToOutput', (payload: CodeOutput) => ({ payload })), + updateReplValue: createAction('repl/updateReplValue', (payload: string) => ({ payload })), +} as const + +export const replReducer = createReducer(defaultRepl, builder => { + builder.addCase(replActions.browseReplHistoryDown, (state) => { + if (state.replHistory.browseIndex === null) { + // Not yet started browsing history, nothing to do + return; + } else if (state.replHistory.browseIndex !== 0) { + // Browsing history, and still have earlier records to show + const newIndex = state.replHistory.browseIndex - 1; + state.replValue = state.replHistory.records[newIndex]; + state.replHistory.browseIndex = newIndex + } else { + // Browsing history, no earlier records to show; return replValue to + // the last value when user started browsing + state.replHistory.browseIndex = null + state.replValue = state.replHistory.originalValue; + } + }) + + builder.addCase(replActions.browseReplHistoryUp, (state) => { + const lastRecords = state.replHistory.records; + const lastIndex = state.replHistory.browseIndex; + if ( + lastRecords.length === 0 || + (lastIndex !== null && lastRecords[lastIndex + 1] === undefined) + ) { + // There is no more later history to show + return; + } else if (lastIndex === null) { + // Not yet started browsing, initialise the index & array + state.replHistory.browseIndex = 0; + state.replHistory.originalValue = state.replValue; + } else { + // Browsing history, and still have later history to show + const newIndex = lastIndex + 1; + state.replValue = lastRecords[newIndex]; + state.replHistory.browseIndex = newIndex + } + }) + + builder.addCase(replActions.clearReplInput, (state) => { state.replValue = '' }) + builder.addCase(replActions.clearReplOutput, (state) => { state.output = [] }) + builder.addCase(replActions.clearReplOutputLast, (state) => { state.output.pop() }) + builder.addCase(replActions.evalInterpreterError, (state, { payload }) => { + const lastOutput = state.output[state.output.length - 1] + state.output.push({ + type: 'errors', + errors: payload, + consoleLogs: lastOutput !== undefined && lastOutput.type === 'running' ? lastOutput.consoleLogs : [] + }) + }) + builder.addCase(replActions.evalInterpreterSuccess, (state, { payload }) => { + const lastOutput = state.output[state.output.length - 1] + state.output.push({ + type: 'result', + value: stringify(payload), + consoleLogs: lastOutput !== undefined && lastOutput.type === 'running' ? lastOutput.consoleLogs : [] + }) + }) + builder.addCase(replActions.handleConsoleLog, (state, { payload }) => { + /* Possible cases: + * (1) state[workspaceLocation].output === [], i.e. state[workspaceLocation].output[-1] === undefined + * (2) state[workspaceLocation].output[-1] is not RunningOutput + * (3) state[workspaceLocation].output[-1] is RunningOutput */ + const lastOutput = state.output[state.output.length - 1]; + if (lastOutput === undefined || lastOutput.type !== 'running') { + // New block of output. state.output.push({ - type: 'result', - value: stringify(payload), - consoleLogs: lastOutput !== undefined && lastOutput.type === 'running' ? lastOutput.consoleLogs : [] - }) - }, - handleConsoleLog(state, { payload }: PayloadAction) { - /* Possible cases: - * (1) state[workspaceLocation].output === [], i.e. state[workspaceLocation].output[-1] === undefined - * (2) state[workspaceLocation].output[-1] is not RunningOutput - * (3) state[workspaceLocation].output[-1] is RunningOutput */ - const lastOutput = state.output[state.output.length - 1]; - if (lastOutput === undefined || lastOutput.type !== 'running') { - // New block of output. - state.output.push({ - type: 'running', - consoleLogs: payload, - }); - } else { - const updatedLastOutput = { - type: lastOutput.type, - consoleLogs: lastOutput.consoleLogs.concat(payload) - }; - state.output[state.output.length - 1] = updatedLastOutput - } - }, - sendReplInputToOutput(state, { payload }: PayloadAction) { - // CodeOutput properties exist in parallel with workspaceLocation - state.output.push(payload) + type: 'running', + consoleLogs: payload, + }); + } else { + const updatedLastOutput = { + type: lastOutput.type, + consoleLogs: lastOutput.consoleLogs.concat(payload) + }; + state.output[state.output.length - 1] = updatedLastOutput + } + }) - if (payload.value !== '') { - state.replHistory.records.unshift(payload.value) - } else { - if (state.replHistory.records.length === Constants.maxBrowseIndex) { - state.replHistory.records.pop() - } + builder.addCase(replActions.sendReplInputToOutput, (state, { payload }) => { + // CodeOutput properties exist in parallel with workspaceLocation + state.output.push(payload) + + if (payload.value !== '') { + state.replHistory.records.unshift(payload.value) + } else { + if (state.replHistory.records.length === Constants.maxBrowseIndex) { + state.replHistory.records.pop() } - }, - updateReplValue(state, { payload }: PayloadAction) { - state.replValue = payload } - } + }) + + builder.addCase(replActions.updateReplValue, (state, { payload }) => { + state.replValue = payload + }) }) diff --git a/src/commons/redux/SafeEffects.ts b/src/commons/redux/SafeEffects.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/commons/redux/SideContentRedux.ts b/src/commons/redux/SideContentRedux.ts index 6b917b91f2..97694d5a34 100644 --- a/src/commons/redux/SideContentRedux.ts +++ b/src/commons/redux/SideContentRedux.ts @@ -1,4 +1,4 @@ -import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit' +import { createAction, createReducer } from '@reduxjs/toolkit' import { getDynamicTabs, getTabId } from '../sideContent/SideContentHelper' import { SideContentTab, SideContentType } from '../sideContent/SideContentTypes' @@ -18,35 +18,35 @@ export const defaultSideContent: SideContentState = { alerts: [] } -const { actions, reducer } = createSlice({ - name: 'sideContent', - initialState: defaultSideContent, - reducers: { - changeSideContentHeight(state, { payload }: PayloadAction) { - state.height = payload - }, - endAlertSideContentHeight(state, { payload: newId }: PayloadAction) { - if (newId === state.selectedTabId) return - - state.alerts.push(newId) - }, - notifyProgramEvaluated(state, { payload }: PayloadAction) { - const dynamicTabs = getDynamicTabs(payload) - state.alerts = dynamicTabs.map(getTabId).filter(id => id !== state.selectedTabId) - state.dynamicTabs = dynamicTabs - }, - visitSideContent(state, { payload: newId }: PayloadAction) { - if (newId === state.selectedTabId) return - - state.alerts = state.alerts.filter(id => id !== newId) - state.selectedTabId = newId - } - } -}) - export const sideContentActions = { - ...actions, - beginAlertSideContent: createAction('sideContent/beginAlertSideContent', (newId: SideContentType) => ({ payload: newId })) + beginAlertSideContent: createAction('sideContent/beginAlertSideContent', (newId: SideContentType) => ({ payload: newId })), + changeSideContentHeight: createAction('sideContent/changeSideContentHeight', (payload: number) => ({ payload })), + endAlertSideContentHeight: createAction('sideContent/endAlertSideContentHeight', (payload: SideContentType) => ({ payload })), + notifyProgramEvaluated: createAction('sideContent/notifyProgramEvaluated', (payload: DebuggerContext) => ({ payload })), + visitSideContent: createAction('sideContent/visitSideContent', (payload: SideContentType) => ({ payload })), } -export { reducer as sideContentReducer } +export const sideContentReducer = createReducer(defaultSideContent, builder => { + builder.addCase(sideContentActions.changeSideContentHeight, (state, { payload }) => { + state.height = payload + }) + + builder.addCase(sideContentActions.endAlertSideContentHeight, (state, { payload: newId }) => { + if (newId === state.selectedTabId) return + + state.alerts.push(newId) + }) + + builder.addCase(sideContentActions.notifyProgramEvaluated, (state, { payload }) => { + const dynamicTabs = getDynamicTabs(payload) + state.alerts = dynamicTabs.map(getTabId).filter(id => id !== state.selectedTabId) + state.dynamicTabs = dynamicTabs + }) + + builder.addCase(sideContentActions.visitSideContent ,(state, { payload: newId }) => { + if (newId === state.selectedTabId) return + + state.alerts = state.alerts.filter(id => id !== newId) + state.selectedTabId = newId + }) +}) diff --git a/src/commons/redux/Sourcereel.ts b/src/commons/redux/Sourcereel.ts new file mode 100644 index 0000000000..134ff1d5b7 --- /dev/null +++ b/src/commons/redux/Sourcereel.ts @@ -0,0 +1,60 @@ +import { PayloadAction } from '@reduxjs/toolkit' + +import { Chapter } from "js-slang/dist/types"; +import { Input, PlaybackData, RecordingStatus } from "src/features/sourceRecorder/SourceRecorderTypes"; + +import { ExternalLibraryName } from "../application/types/ExternalTypes"; +import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "./workspace/WorkspaceRedux"; + +export type SourcereelState = { + readonly playbackData: PlaybackData; + readonly recordingStatus: RecordingStatus; + readonly timeElapsedBeforePause: number; + readonly timeResumed: number; +} & WorkspaceState + +export const defaultSourcereel: SourcereelState = { + ...getDefaultWorkspaceState(), + playbackData: { + init: { + editorValue: '', + chapter: Chapter.SOURCE_1, + externalLibrary: ExternalLibraryName.NONE + }, + inputs: [] + }, + recordingStatus: RecordingStatus.notStarted, + timeElapsedBeforePause: 0, + timeResumed: 0 +} + +export const { actions: sourcereelActions, reducer: sourcereelReducer } = createWorkspaceSlice('sourcereel', defaultSourcereel, { + recordInit(state, { payload }: PayloadAction) { + state.playbackData = { + init: payload, + inputs: [] + } + }, + recordInput(state, { payload }: PayloadAction) { + state.playbackData.inputs.push(payload) + }, + resetInputs(state, { payload }: PayloadAction) { + state.playbackData.inputs = payload + }, + timerPause(state, { payload }: PayloadAction) { + state.recordingStatus = RecordingStatus.paused + state.timeElapsedBeforePause = state.timeElapsedBeforePause + payload - state.timeResumed + }, + timerReset(state) { + state.recordingStatus = RecordingStatus.notStarted + state.timeElapsedBeforePause = 0 + state.timeResumed = 0 + }, + timerResume: { + reducer(state, { payload: { timeNow, timeBefore } }: PayloadAction>) { + state.recordingStatus = RecordingStatus.recording + state.timeElapsedBeforePause = timeBefore >= 0 ? timeBefore : state.timeElapsedBeforePause + state.timeResumed = timeNow + } + } +}) diff --git a/src/commons/redux/workspace/AllWorkspacesRedux.ts b/src/commons/redux/workspace/AllWorkspacesRedux.ts index 38fcc1b7d9..224a1a92a9 100644 --- a/src/commons/redux/workspace/AllWorkspacesRedux.ts +++ b/src/commons/redux/workspace/AllWorkspacesRedux.ts @@ -1,31 +1,38 @@ -import { ActionCreatorWithPreparedPayload, PayloadAction, PayloadActionCreator } from "@reduxjs/toolkit"; -import { SourceActionType } from "src/commons/utils/ActionsHelper"; -import { StoriesState } from "src/features/stories/StoriesTypes"; +import { Action, ActionCreatorWithPreparedPayload, combineReducers, createAction, createReducer, PayloadAction, PayloadActionCreator } from "@reduxjs/toolkit"; -import { SideContentLocation } from "../SideContentRedux"; -import { getDefaultStoriesEnv } from "../StoriesRedux"; +import { replActions } from "../ReplRedux"; +import { sideContentActions, SideContentLocation } from "../SideContentRedux"; +import { defaultStories, getDefaultStoriesEnv, storiesReducer, StoriesState } from "../StoriesRedux"; +import { editorActions } from "./EditorRedux"; +import { defaultGradingState, gradingReducer, GradingWorkspaceState } from "./GradingRedux"; import { basePlaygroundReducer } from "./playground/PlaygroundBase"; -import { playgroundReducer,PlaygroundState } from "./playground/PlaygroundRedux"; -import { createWorkspaceSlice, getDefaultWorkspaceState } from "./WorkspaceRedux"; - -const { actions } = createWorkspaceSlice('sicp', getDefaultWorkspaceState([]), { - testAction(state) {} -}) +import { defaultPlayground, playgroundReducer, PlaygroundState } from "./playground/PlaygroundRedux"; +import { workspaceActions } from "./WorkspaceRedux"; type WorkspaceManagerState = { + grading: GradingWorkspaceState, playground: PlaygroundState stories: StoriesState } -type AllWorkspaceActions = { - [K in keyof typeof actions]: ActionCreatorWithPreparedPayload< - [location: SideContentLocation, ...Parameters], - { payload: ReturnType['payload'], location: SideContentLocation }, - (typeof actions)[K]['type'] +const commonWorkspaceActionsInternal = { + ...editorActions, + ...replActions, + ...sideContentActions, + ...workspaceActions, +} + +type CommonWorkspaceActions = { + [K in keyof typeof commonWorkspaceActionsInternal]: ActionCreatorWithPreparedPayload< + [location: SideContentLocation, ...Parameters], + { payload: ReturnType['payload'], location: SideContentLocation }, + (typeof commonWorkspaceActionsInternal)[K]['type'] > } -export const allWorkspaceActions = Object.entries(actions).reduce((res, [name, creator]) => ({ +type CommonWorkspaceAction = PayloadAction<{ payload: T, location: SideContentLocation }> + +const commonWorkspaceActions = Object.entries(commonWorkspaceActionsInternal).reduce((res, [name, creator]) => ({ ...res, [name]: (location: SideContentLocation, ...args: any) => { // @ts-ignore @@ -38,12 +45,50 @@ export const allWorkspaceActions = Object.entries(actions).reduce((res, [name, c } } } -}), {} as AllWorkspaceActions) +}), {} as CommonWorkspaceActions) + +const commonWorkspaceActionTypes = Object.keys(commonWorkspaceActions) + +export const allWorkspaceActions = { + ...commonWorkspaceActions, + logOut: createAction('workspaces/logOut') +} const allWorkspaceReducers = { + grading: gradingReducer, playground: playgroundReducer, + stories: storiesReducer, +} + +const workspaceManagerReducer = combineReducers(allWorkspaceReducers) + +const defaultWorkspaceManager: WorkspaceManagerState = { + grading: defaultGradingState, + playground: defaultPlayground, + stories: defaultStories, } +const isCommonWorkspaceAction = (action: Action): action is CommonWorkspaceAction => commonWorkspaceActionTypes.includes[action.type] + +export const allWorkspacesReducer = createReducer(defaultWorkspaceManager, builder => { + builder.addMatcher(isCommonWorkspaceAction, (state, { payload: { payload, location }, ...action }) => { + const newAction = { + ...action, + payload, + } + + if (location.startsWith('stories')) { + const [, storyEnv] = location.split('.') + const storyReducer = basePlaygroundReducer(getDefaultStoriesEnv(storyEnv)) + state.stories.envs[storyEnv] = storyReducer(state.stories[storyEnv], action) + } else { + state[location] = allWorkspaceReducers[location](state[location], newAction) + } + }) + builder.addDefaultCase((state, action) => workspaceManagerReducer(state as WorkspaceManagerState, action)) +}) + +/* export function allWorkspacesReducer(state: WorkspaceManagerState, action: SourceActionType) { let workspaceLocation: SideContentLocation if ((action as any).location) { @@ -82,3 +127,4 @@ export function allWorkspacesReducer(state: WorkspaceManagerState, action: Sourc } } } +*/ diff --git a/src/commons/redux/workspace/EditorRedux.ts b/src/commons/redux/workspace/EditorRedux.ts index 0a7292fbae..86c7b23976 100644 --- a/src/commons/redux/workspace/EditorRedux.ts +++ b/src/commons/redux/workspace/EditorRedux.ts @@ -1,4 +1,4 @@ -import { createSlice,PayloadAction } from "@reduxjs/toolkit" +import { createAction, createReducer } from "@reduxjs/toolkit" import { HighlightedLines, Position } from "src/commons/editor/EditorTypes" import { EditorTabState } from "src/commons/workspace/WorkspaceTypes" @@ -19,52 +19,63 @@ export const getDefaultEditorState = (defaultTabs: EditorTabState[] = []): Edito isEditorReadonly: false }) -export const getEditorSlice = (defaultTabs: EditorTabState[] = []) => createSlice({ - name: 'editor', - initialState: getDefaultEditorState(defaultTabs), - reducers: { - addEditorTab: { - prepare: (filePath: string, editorValue: string) => ({ payload: { filePath, editorValue }}), - reducer(state, { payload }: PayloadAction<{ filePath: string, editorValue: string}>) { - const { filePath, editorValue } = payload; - - const editorTabs = state.editorTabs; - const openedEditorTabIndex = editorTabs.findIndex( - (editorTab: EditorTabState) => editorTab.filePath === filePath - ); - const fileIsAlreadyOpen = openedEditorTabIndex !== -1; - if (fileIsAlreadyOpen) { - // If the file is already opened just swap to the tab - state.activeEditorTabIndex = openedEditorTabIndex - return - } +export const editorActions = { + addEditorTab: createAction('editorBase/addEditorTab', (filePath: string, editorValue: string) => ({ payload: { filePath, editorValue }})), + moveCursor: createAction('editorBase/moveCursor', (editorTabIndex: number, newCursorPosition: Position) => ({ payload: { editorTabIndex, newCursorPosition }})), + removeEditorTab: createAction('editorBase/removeEditorTab', (editorTabIndex: number) => ({ payload: editorTabIndex })), + setEditorSessionId: createAction('editorBase/setEditorSessionId', (payload: string) => ({ payload })), + setIsEditorAutorun: createAction('editorBase/setIsEditorAutorun', (payload: boolean) => ({ payload })), + setIsEditorReadonly: createAction('editorBase/setIsEditorReadonly', (payload: boolean) => ({ payload })), + shiftEditorTab: createAction('editorAction/shiftEditorTab', (previousIndex: number, newIndex: number) => ({ payload: { previousIndex, newIndex }})), + updateActiveEditorTab: createAction('editorBase/updateActiveEditorTab', (payload: Partial | undefined) => ({ payload })), + updateActiveEditorTabIndex: createAction('editorBase/updateActiveEditorTabIndex', (payload: number | null) => ({ payload })), + updateEditorBreakpoints: createAction('editorBase/updateEditorBreakpoints', (editorTabIndex: number, newBreakpoints: string[]) => ({ payload: { editorTabIndex, newBreakpoints }})), + updateEditorHighlightedLines: createAction('editorBase/updateEditorHighlightedLines', (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }})), + updateEditorHighlightedLinesAgenda: createAction('editorBase/updateEditorHighlightedLinesAgenda', (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }})), + updateEditorValue: createAction('editorBase/updateEditorValue', (editorTabIndex: number, newEditorValue: string) => ({ payload: { editorTabIndex, newEditorValue }})), +} as const - state.editorTabs.push({ - filePath, - value: editorValue, - highlightedLines: [], - breakpoints: [] - }) - - // Check if this works properly - state.activeEditorTabIndex = state.editorTabs.length + 1 - } - }, - moveCursor: { - prepare: (editorTabIndex: number, newCursorPosition: Position) => ({ payload: { editorTabIndex, newCursorPosition }}), - reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newCursorPosition: Position }>) { - const { editorTabIndex, newCursorPosition } = payload; - if (editorTabIndex < 0) { - throw new Error('Editor tab index must be non-negative!'); - } - if (editorTabIndex >= state.editorTabs.length) { - throw new Error('Editor tab index must have a corresponding editor tab!'); - } +export const getEditorReducer = (defaultTabs: EditorTabState[] = [] ) => createReducer( + getDefaultEditorState(defaultTabs), + builder => { + builder.addCase(editorActions.addEditorTab, (state, { payload }) => { + const { filePath, editorValue } = payload; - state.editorTabs[editorTabIndex].newCursorPosition = newCursorPosition + const editorTabs = state.editorTabs; + const openedEditorTabIndex = editorTabs.findIndex( + (editorTab: EditorTabState) => editorTab.filePath === filePath + ); + const fileIsAlreadyOpen = openedEditorTabIndex !== -1; + if (fileIsAlreadyOpen) { + // If the file is already opened just swap to the tab + state.activeEditorTabIndex = openedEditorTabIndex + return } - }, - removeEditorTab(state, { payload: editorTabIndex }: PayloadAction) { + + state.editorTabs.push({ + filePath, + value: editorValue, + highlightedLines: [], + breakpoints: [] + }) + + // Check if this works properly + state.activeEditorTabIndex = state.editorTabs.length + 1 + }) + + builder.addCase(editorActions.moveCursor, (state, { payload }) => { + const { editorTabIndex, newCursorPosition } = payload; + if (editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + + state.editorTabs[editorTabIndex].newCursorPosition = newCursorPosition + }) + + builder.addCase(editorActions.removeEditorTab, (state, { payload: editorTabIndex }) => { if (editorTabIndex < 0) { throw new Error('Editor tab index must be non-negative!'); } @@ -81,49 +92,51 @@ export const getEditorSlice = (defaultTabs: EditorTabState[] = []) => createSlic state.activeEditorTabIndex = newActiveEditorTabIndex state.editorTabs.splice(editorTabIndex, 1) - }, - setEditorSessionId(state, { payload }: PayloadAction) { + }) + + builder.addCase(editorActions.setEditorSessionId, (state, { payload }) => { state.editorSessionId = payload - }, - setIsEditorAutorun(state, { payload }: PayloadAction) { + }) + + builder.addCase(editorActions.setIsEditorAutorun, (state, { payload }) => { state.isEditorAutorun = payload - }, - setIsEditorReadonly(state, { payload }: PayloadAction) { + }) + + builder.addCase(editorActions.setIsEditorReadonly, (state, { payload }) => { state.isEditorReadonly = payload - }, - shiftEditorTab: { - prepare: (previousIndex: number, newIndex: number) => ({ payload: { previousIndex, newIndex }}), - reducer(state, action: PayloadAction<{ previousIndex: number, newIndex: number}>) { - const { previousIndex, newIndex } = action.payload; - if (previousIndex < 0) { - throw new Error('Previous editor tab index must be non-negative!'); - } - if (previousIndex >= state.editorTabs.length) { - throw new Error('Previous editor tab index must have a corresponding editor tab!'); - } - if (newIndex < 0) { - throw new Error('New editor tab index must be non-negative!'); - } - if (newIndex >= state.editorTabs.length) { - throw new Error('New editor tab index must have a corresponding editor tab!'); - } - state.activeEditorTabIndex = - state.activeEditorTabIndex === previousIndex - ? newIndex - : state.activeEditorTabIndex; - const editorTabs = state.editorTabs; - const shiftedEditorTab = editorTabs[previousIndex]; - const filteredEditorTabs = editorTabs.filter( - (editorTab: EditorTabState, index: number) => index !== previousIndex - ); - state.editorTabs = [ - ...filteredEditorTabs.slice(0, newIndex), - shiftedEditorTab, - ...filteredEditorTabs.slice(newIndex) - ]; - } - }, - updateActiveEditorTab(state, { payload: activeEditorTabOptions }: PayloadAction | undefined>) { + }) + + builder.addCase(editorActions.shiftEditorTab, (state, action) => { + const { previousIndex, newIndex } = action.payload; + if (previousIndex < 0) { + throw new Error('Previous editor tab index must be non-negative!'); + } + if (previousIndex >= state.editorTabs.length) { + throw new Error('Previous editor tab index must have a corresponding editor tab!'); + } + if (newIndex < 0) { + throw new Error('New editor tab index must be non-negative!'); + } + if (newIndex >= state.editorTabs.length) { + throw new Error('New editor tab index must have a corresponding editor tab!'); + } + state.activeEditorTabIndex = + state.activeEditorTabIndex === previousIndex + ? newIndex + : state.activeEditorTabIndex; + const editorTabs = state.editorTabs; + const shiftedEditorTab = editorTabs[previousIndex]; + const filteredEditorTabs = editorTabs.filter( + (editorTab: EditorTabState, index: number) => index !== previousIndex + ); + state.editorTabs = [ + ...filteredEditorTabs.slice(0, newIndex), + shiftedEditorTab, + ...filteredEditorTabs.slice(newIndex) + ]; + }) + + builder.addCase(editorActions.updateActiveEditorTab, (state, { payload: activeEditorTabOptions }) => { const activeEditorTabIndex = state.activeEditorTabIndex; // Do not modify the workspace state if there is no active editor tab. if (activeEditorTabIndex === null) return @@ -132,8 +145,9 @@ export const getEditorSlice = (defaultTabs: EditorTabState[] = []) => createSlic ...state.editorTabs[activeEditorTabIndex], ...activeEditorTabOptions } - }, - updateActiveEditorTabIndex(state, { payload: activeEditorTabIndex }: PayloadAction) { + }) + + builder.addCase(editorActions.updateActiveEditorTabIndex, (state, { payload: activeEditorTabIndex }) => { if (activeEditorTabIndex !== null) { if (activeEditorTabIndex < 0) { throw new Error('Active editor tab index must be non-negative!'); @@ -143,57 +157,49 @@ export const getEditorSlice = (defaultTabs: EditorTabState[] = []) => createSlic } } state.activeEditorTabIndex = activeEditorTabIndex - }, - updateEditorBreakpoints: { - prepare: (editorTabIndex: number, newBreakpoints: string[]) => ({ payload: { editorTabIndex, newBreakpoints }}), - reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newBreakpoints: string[] }>) { - if (payload.editorTabIndex < 0) { - throw new Error('Editor tab index must be non-negative!'); - } - if (payload.editorTabIndex >= state.editorTabs.length) { - throw new Error('Editor tab index must have a corresponding editor tab!'); - } - state.editorTabs[payload.editorTabIndex].breakpoints = payload.newBreakpoints - } - }, - updateEditorHighlightedLines: { - prepare: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }}), - reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newHighlightedLines: HighlightedLines[] }>) { - if (payload.editorTabIndex < 0) { - throw new Error('Editor tab index must be non-negative!'); - } - if (payload.editorTabIndex >= state.editorTabs.length) { - throw new Error('Editor tab index must have a corresponding editor tab!'); - } - state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines - } - }, - updateEditorHighlightedLinesAgenda: { - prepare: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }}), - reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newHighlightedLines: HighlightedLines[] }>) { - if (payload.editorTabIndex < 0) { - throw new Error('Editor tab index must be non-negative!'); - } - if (payload.editorTabIndex >= state.editorTabs.length) { - throw new Error('Editor tab index must have a corresponding editor tab!'); - } - state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines - } - }, - updateEditorValue: { - prepare: (editorTabIndex: number, newEditorValue: string) => ({ payload: { editorTabIndex, newEditorValue }}), - reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newEditorValue: string}>) { - if (payload.editorTabIndex < 0) { - throw new Error('Editor tab index must be non-negative!'); - } - if (payload.editorTabIndex >= state.editorTabs.length) { - throw new Error('Editor tab index must have a corresponding editor tab!'); - } - state.editorTabs[payload.editorTabIndex].value = payload.newEditorValue + }) + + builder.addCase(editorActions.updateEditorBreakpoints, (state, { payload }) => { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); } - }, + state.editorTabs[payload.editorTabIndex].breakpoints = payload.newBreakpoints + }) + + builder.addCase(editorActions.updateEditorHighlightedLines, (state, { payload }) => { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines + }) + + builder.addCase(editorActions.updateEditorHighlightedLinesAgenda, (state, { payload }) => { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines + }) + + builder.addCase(editorActions.updateEditorValue, (state, { payload }) => { + if (payload.editorTabIndex < 0) { + throw new Error('Editor tab index must be non-negative!'); + } + if (payload.editorTabIndex >= state.editorTabs.length) { + throw new Error('Editor tab index must have a corresponding editor tab!'); + } + state.editorTabs[payload.editorTabIndex].value = payload.newEditorValue + }) } -}) +) const getNextActiveEditorTabIndexAfterTabRemoval = ( activeEditorTabIndex: number | null, @@ -222,3 +228,342 @@ const getNextActiveEditorTabIndexAfterTabRemoval = ( // left of the removed tab. removedEditorTabIndex - 1; }; + +// export const getEditorSlice = (defaultTabs: EditorTabState[] = []) => createSlice({ +// name: 'editor', +// initialState: getDefaultEditorState(defaultTabs), +// reducers: {}, +// // reducers: { +// // addEditorTab: { +// // prepare: (filePath: string, editorValue: string) => ({ payload: { filePath, editorValue }}), +// // reducer(state, { payload }: PayloadAction<{ filePath: string, editorValue: string}>) { +// // const { filePath, editorValue } = payload; + +// // const editorTabs = state.editorTabs; +// // const openedEditorTabIndex = editorTabs.findIndex( +// // (editorTab: EditorTabState) => editorTab.filePath === filePath +// // ); +// // const fileIsAlreadyOpen = openedEditorTabIndex !== -1; +// // if (fileIsAlreadyOpen) { +// // // If the file is already opened just swap to the tab +// // state.activeEditorTabIndex = openedEditorTabIndex +// // return +// // } + +// // state.editorTabs.push({ +// // filePath, +// // value: editorValue, +// // highlightedLines: [], +// // breakpoints: [] +// // }) + +// // // Check if this works properly +// // state.activeEditorTabIndex = state.editorTabs.length + 1 +// // } +// // }, +// // moveCursor: { +// // prepare: (editorTabIndex: number, newCursorPosition: Position) => ({ payload: { editorTabIndex, newCursorPosition }}), +// // reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newCursorPosition: Position }>) { +// // const { editorTabIndex, newCursorPosition } = payload; +// // if (editorTabIndex < 0) { +// // throw new Error('Editor tab index must be non-negative!'); +// // } +// // if (editorTabIndex >= state.editorTabs.length) { +// // throw new Error('Editor tab index must have a corresponding editor tab!'); +// // } + +// // state.editorTabs[editorTabIndex].newCursorPosition = newCursorPosition +// // } +// // }, +// // removeEditorTab(state, { payload: editorTabIndex }: PayloadAction) { +// // if (editorTabIndex < 0) { +// // throw new Error('Editor tab index must be non-negative!'); +// // } +// // if (editorTabIndex >= state.editorTabs.length) { +// // throw new Error('Editor tab index must have a corresponding editor tab!'); +// // } + +// // const activeEditorTabIndex = state.activeEditorTabIndex; +// // const newActiveEditorTabIndex = getNextActiveEditorTabIndexAfterTabRemoval( +// // activeEditorTabIndex, +// // editorTabIndex, +// // state.editorTabs.length - 1 +// // ); + +// // state.activeEditorTabIndex = newActiveEditorTabIndex +// // state.editorTabs.splice(editorTabIndex, 1) +// // }, +// // setEditorSessionId(state, { payload }: PayloadAction) { +// // state.editorSessionId = payload +// // }, +// // setIsEditorAutorun(state, { payload }: PayloadAction) { +// // state.isEditorAutorun = payload +// // }, +// // setIsEditorReadonly(state, { payload }: PayloadAction) { +// // state.isEditorReadonly = payload +// // }, +// // shiftEditorTab: { +// // prepare: (previousIndex: number, newIndex: number) => ({ payload: { previousIndex, newIndex }}), +// // reducer(state, action: PayloadAction<{ previousIndex: number, newIndex: number}>) { +// // const { previousIndex, newIndex } = action.payload; +// // if (previousIndex < 0) { +// // throw new Error('Previous editor tab index must be non-negative!'); +// // } +// // if (previousIndex >= state.editorTabs.length) { +// // throw new Error('Previous editor tab index must have a corresponding editor tab!'); +// // } +// // if (newIndex < 0) { +// // throw new Error('New editor tab index must be non-negative!'); +// // } +// // if (newIndex >= state.editorTabs.length) { +// // throw new Error('New editor tab index must have a corresponding editor tab!'); +// // } +// // state.activeEditorTabIndex = +// // state.activeEditorTabIndex === previousIndex +// // ? newIndex +// // : state.activeEditorTabIndex; +// // const editorTabs = state.editorTabs; +// // const shiftedEditorTab = editorTabs[previousIndex]; +// // const filteredEditorTabs = editorTabs.filter( +// // (editorTab: EditorTabState, index: number) => index !== previousIndex +// // ); +// // state.editorTabs = [ +// // ...filteredEditorTabs.slice(0, newIndex), +// // shiftedEditorTab, +// // ...filteredEditorTabs.slice(newIndex) +// // ]; +// // } +// // }, +// // updateActiveEditorTab(state, { payload: activeEditorTabOptions }: PayloadAction | undefined>) { +// // const activeEditorTabIndex = state.activeEditorTabIndex; +// // // Do not modify the workspace state if there is no active editor tab. +// // if (activeEditorTabIndex === null) return + +// // state.editorTabs[activeEditorTabIndex] = { +// // ...state.editorTabs[activeEditorTabIndex], +// // ...activeEditorTabOptions +// // } +// // }, +// // updateActiveEditorTabIndex(state, { payload: activeEditorTabIndex }: PayloadAction) { +// // if (activeEditorTabIndex !== null) { +// // if (activeEditorTabIndex < 0) { +// // throw new Error('Active editor tab index must be non-negative!'); +// // } +// // if (activeEditorTabIndex >= state.editorTabs.length) { +// // throw new Error('Active editor tab index must have a corresponding editor tab!'); +// // } +// // } +// // state.activeEditorTabIndex = activeEditorTabIndex +// // }, +// // updateEditorBreakpoints: { +// // prepare: (editorTabIndex: number, newBreakpoints: string[]) => ({ payload: { editorTabIndex, newBreakpoints }}), +// // reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newBreakpoints: string[] }>) { +// // if (payload.editorTabIndex < 0) { +// // throw new Error('Editor tab index must be non-negative!'); +// // } +// // if (payload.editorTabIndex >= state.editorTabs.length) { +// // throw new Error('Editor tab index must have a corresponding editor tab!'); +// // } +// // state.editorTabs[payload.editorTabIndex].breakpoints = payload.newBreakpoints +// // } +// // }, +// // updateEditorHighlightedLines: { +// // prepare: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }}), +// // reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newHighlightedLines: HighlightedLines[] }>) { +// // if (payload.editorTabIndex < 0) { +// // throw new Error('Editor tab index must be non-negative!'); +// // } +// // if (payload.editorTabIndex >= state.editorTabs.length) { +// // throw new Error('Editor tab index must have a corresponding editor tab!'); +// // } +// // state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines +// // } +// // }, +// // updateEditorHighlightedLinesAgenda: { +// // prepare: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }}), +// // reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newHighlightedLines: HighlightedLines[] }>) { +// // if (payload.editorTabIndex < 0) { +// // throw new Error('Editor tab index must be non-negative!'); +// // } +// // if (payload.editorTabIndex >= state.editorTabs.length) { +// // throw new Error('Editor tab index must have a corresponding editor tab!'); +// // } +// // state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines +// // } +// // }, +// // updateEditorValue: { +// // prepare: (editorTabIndex: number, newEditorValue: string) => ({ payload: { editorTabIndex, newEditorValue }}), +// // reducer(state, { payload }: PayloadAction<{ editorTabIndex: number, newEditorValue: string}>) { +// // if (payload.editorTabIndex < 0) { +// // throw new Error('Editor tab index must be non-negative!'); +// // } +// // if (payload.editorTabIndex >= state.editorTabs.length) { +// // throw new Error('Editor tab index must have a corresponding editor tab!'); +// // } +// // state.editorTabs[payload.editorTabIndex].value = payload.newEditorValue +// // } +// // }, +// // }, +// extraReducers: builder => { +// builder.addCase(editorActions.addEditorTab, (state, { payload }) => { +// const { filePath, editorValue } = payload; + +// const editorTabs = state.editorTabs; +// const openedEditorTabIndex = editorTabs.findIndex( +// (editorTab: EditorTabState) => editorTab.filePath === filePath +// ); +// const fileIsAlreadyOpen = openedEditorTabIndex !== -1; +// if (fileIsAlreadyOpen) { +// // If the file is already opened just swap to the tab +// state.activeEditorTabIndex = openedEditorTabIndex +// return +// } + +// state.editorTabs.push({ +// filePath, +// value: editorValue, +// highlightedLines: [], +// breakpoints: [] +// }) + +// // Check if this works properly +// state.activeEditorTabIndex = state.editorTabs.length + 1 +// }) + +// builder.addCase(editorActions.moveCursor, (state, { payload }) => { +// const { editorTabIndex, newCursorPosition } = payload; +// if (editorTabIndex < 0) { +// throw new Error('Editor tab index must be non-negative!'); +// } +// if (editorTabIndex >= state.editorTabs.length) { +// throw new Error('Editor tab index must have a corresponding editor tab!'); +// } + +// state.editorTabs[editorTabIndex].newCursorPosition = newCursorPosition +// }) + +// builder.addCase(editorActions.removeEditorTab, (state, { payload: editorTabIndex }) => { +// if (editorTabIndex < 0) { +// throw new Error('Editor tab index must be non-negative!'); +// } +// if (editorTabIndex >= state.editorTabs.length) { +// throw new Error('Editor tab index must have a corresponding editor tab!'); +// } + +// const activeEditorTabIndex = state.activeEditorTabIndex; +// const newActiveEditorTabIndex = getNextActiveEditorTabIndexAfterTabRemoval( +// activeEditorTabIndex, +// editorTabIndex, +// state.editorTabs.length - 1 +// ); + +// state.activeEditorTabIndex = newActiveEditorTabIndex +// state.editorTabs.splice(editorTabIndex, 1) +// }) + +// builder.addCase(editorActions.setEditorSessionId, (state, { payload }) => { +// state.editorSessionId = payload +// }) + +// builder.addCase(editorActions.setIsEditorAutorun, (state, { payload }) => { +// state.isEditorAutorun = payload +// }) + +// builder.addCase(editorActions.setIsEditorReadonly, (state, { payload }) => { +// state.isEditorReadonly = payload +// }) + +// builder.addCase(editorActions.shiftEditorTab, (state, action) => { +// const { previousIndex, newIndex } = action.payload; +// if (previousIndex < 0) { +// throw new Error('Previous editor tab index must be non-negative!'); +// } +// if (previousIndex >= state.editorTabs.length) { +// throw new Error('Previous editor tab index must have a corresponding editor tab!'); +// } +// if (newIndex < 0) { +// throw new Error('New editor tab index must be non-negative!'); +// } +// if (newIndex >= state.editorTabs.length) { +// throw new Error('New editor tab index must have a corresponding editor tab!'); +// } +// state.activeEditorTabIndex = +// state.activeEditorTabIndex === previousIndex +// ? newIndex +// : state.activeEditorTabIndex; +// const editorTabs = state.editorTabs; +// const shiftedEditorTab = editorTabs[previousIndex]; +// const filteredEditorTabs = editorTabs.filter( +// (editorTab: EditorTabState, index: number) => index !== previousIndex +// ); +// state.editorTabs = [ +// ...filteredEditorTabs.slice(0, newIndex), +// shiftedEditorTab, +// ...filteredEditorTabs.slice(newIndex) +// ]; +// }) + +// builder.addCase(editorActions.updateActiveEditorTab, (state, { payload: activeEditorTabOptions }) => { +// const activeEditorTabIndex = state.activeEditorTabIndex; +// // Do not modify the workspace state if there is no active editor tab. +// if (activeEditorTabIndex === null) return + +// state.editorTabs[activeEditorTabIndex] = { +// ...state.editorTabs[activeEditorTabIndex], +// ...activeEditorTabOptions +// } +// }) + +// builder.addCase(editorActions.updateActiveEditorTabIndex, (state, { payload: activeEditorTabIndex }) => { +// if (activeEditorTabIndex !== null) { +// if (activeEditorTabIndex < 0) { +// throw new Error('Active editor tab index must be non-negative!'); +// } +// if (activeEditorTabIndex >= state.editorTabs.length) { +// throw new Error('Active editor tab index must have a corresponding editor tab!'); +// } +// } +// state.activeEditorTabIndex = activeEditorTabIndex +// }) + +// builder.addCase(editorActions.updateEditorBreakpoints, (state, { payload }) => { +// if (payload.editorTabIndex < 0) { +// throw new Error('Editor tab index must be non-negative!'); +// } +// if (payload.editorTabIndex >= state.editorTabs.length) { +// throw new Error('Editor tab index must have a corresponding editor tab!'); +// } +// state.editorTabs[payload.editorTabIndex].breakpoints = payload.newBreakpoints +// }) + +// builder.addCase(editorActions.updateEditorHighlightedLines, (state, { payload }) => { +// if (payload.editorTabIndex < 0) { +// throw new Error('Editor tab index must be non-negative!'); +// } +// if (payload.editorTabIndex >= state.editorTabs.length) { +// throw new Error('Editor tab index must have a corresponding editor tab!'); +// } +// state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines +// }) + +// builder.addCase(editorActions.updateEditorHighlightedLinesAgenda, (state, { payload }) => { +// if (payload.editorTabIndex < 0) { +// throw new Error('Editor tab index must be non-negative!'); +// } +// if (payload.editorTabIndex >= state.editorTabs.length) { +// throw new Error('Editor tab index must have a corresponding editor tab!'); +// } +// state.editorTabs[payload.editorTabIndex].highlightedLines = payload.newHighlightedLines +// }) + +// builder.addCase(editorActions.updateEditorValue, (state, { payload }) => { +// if (payload.editorTabIndex < 0) { +// throw new Error('Editor tab index must be non-negative!'); +// } +// if (payload.editorTabIndex >= state.editorTabs.length) { +// throw new Error('Editor tab index must have a corresponding editor tab!'); +// } +// state.editorTabs[payload.editorTabIndex].value = payload.newEditorValue +// }) +// } +// }) diff --git a/src/commons/redux/workspace/GradingRedux.ts b/src/commons/redux/workspace/GradingRedux.ts new file mode 100644 index 0000000000..9444baadec --- /dev/null +++ b/src/commons/redux/workspace/GradingRedux.ts @@ -0,0 +1,35 @@ +import { PayloadAction } from '@reduxjs/toolkit' +import { SubmissionsTableFilters } from "src/commons/workspace/WorkspaceTypes"; + +import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "./WorkspaceRedux"; + +export type GradingWorkspaceState = WorkspaceState & { + readonly submissionsTableFilters: SubmissionsTableFilters; + readonly currentSubmission?: number; + readonly currentQuestion?: number; + readonly hasUnsavedChanges: boolean; +} + +export const defaultGradingState: GradingWorkspaceState = ({ + ...getDefaultWorkspaceState(), + submissionsTableFilters: { + columnFilters: [], + globalFilter: null + }, + currentSubmission: undefined, + currentQuestion: undefined, + hasUnsavedChanges: false +}) + +export const { actions: gradingActions, reducer: gradingReducer } = createWorkspaceSlice('grading', defaultGradingState, { + updateCurrentSubmissionId: { + prepare: (currentSubmission: number, currentQuestion: number) => ({ payload: { currentQuestion, currentSubmission }}), + reducer(state, { payload }: PayloadAction<{ currentSubmission: number, currentQuestion: number }>) { + state.currentQuestion = payload.currentQuestion + state.currentSubmission = payload.currentSubmission + } + }, + updateSubmissionsTableFilters(state, { payload }: PayloadAction) { + state.submissionsTableFilters = payload + } +}) diff --git a/src/commons/redux/workspace/WorkspaceRedux.ts b/src/commons/redux/workspace/WorkspaceRedux.ts index f981dbc366..91ec30db5c 100644 --- a/src/commons/redux/workspace/WorkspaceRedux.ts +++ b/src/commons/redux/workspace/WorkspaceRedux.ts @@ -1,4 +1,4 @@ -import { combineReducers, createSlice, Draft, PayloadAction, SliceCaseReducers } from "@reduxjs/toolkit"; +import { ActionReducerMapBuilder, combineReducers, createAction, createSlice, SliceCaseReducers,ValidateSliceCaseReducers } from "@reduxjs/toolkit"; import { Context } from "js-slang/dist/types"; import { InterpreterOutput } from "src/commons/application/ApplicationTypes"; import Constants from "src/commons/utils/Constants"; @@ -7,7 +7,7 @@ import { DebuggerContext, EditorTabState } from "src/commons/workspace/Workspace import { defaultRepl,replActions,replReducer,ReplState } from "../ReplRedux"; import { defaultSideContent, sideContentActions, sideContentReducer, SideContentState } from "../SideContentRedux"; -import { EditorState, getDefaultEditorState, getEditorSlice } from "./EditorRedux"; +import { EditorState, getDefaultEditorState, getEditorReducer } from "./EditorRedux"; export type WorkspaceState = { readonly context: Context; @@ -58,40 +58,54 @@ export const getDefaultWorkspaceState = (initialTabs: EditorTabState[] = []): Wo sideContent: defaultSideContent, }) -const workspaceReducers = { - debugReset(state: Draft) { - state.isDebugging = false; - state.isRunning = false; - }, - debugResume(state: Draft) { - state.isDebugging = false; - state.isRunning = true; - }, - endClearContext(state: Draft) { - // TODO Investigate - }, - endDebugPause(state: Draft) { - state.isDebugging = true; - state.isRunning = false; - }, - endInterruptExecution(state: Draft) { - // same as debug reset - state.isDebugging = false; - state.isRunning = false; - }, - evalEditor(state: Draft) { - state.isDebugging = false; - state.isRunning = true; - }, - evalRepl(state: Draft) { - state.isRunning = true; - }, - setFolderMode(state: Draft, { payload }: PayloadAction) { - state.isFolderModeEnabled = payload; - }, +export const workspaceActions = { + beginClearContext: createAction('workspace/beginClearContext'), + beginDebugPause: createAction('workspace/beginDebugPause'), + beginInterruptExecution: createAction('workspace/beginInterruptExecution'), + debugReset: createAction('workspace/debugReset'), + debugResume: createAction('workspace/debugResume'), + endClearContext: createAction('workspace/endClearContext'), + endDebugPause: createAction('workspace/endDebugPause'), + endInterruptExecution: createAction('workspace/endInterruptExecution'), + evalEditor: createAction('workspace/evalEditor'), + evalRepl: createAction('workspace/evalRepl'), + setFolderMode: createAction('workspace/setFolderMode', (value: boolean) => ({ payload: value })), } as const -type BaseWorkspaceReducers = typeof workspaceReducers +// const workspaceReducers = { +// debugReset(state: Draft) { +// state.isDebugging = false; +// state.isRunning = false; +// }, +// debugResume(state: Draft) { +// state.isDebugging = false; +// state.isRunning = true; +// }, +// endClearContext(state: Draft) { +// // TODO Investigate +// }, +// endDebugPause(state: Draft) { +// state.isDebugging = true; +// state.isRunning = false; +// }, +// endInterruptExecution(state: Draft) { +// // same as debug reset +// state.isDebugging = false; +// state.isRunning = false; +// }, +// evalEditor(state: Draft) { +// state.isDebugging = false; +// state.isRunning = true; +// }, +// evalRepl(state: Draft) { +// state.isRunning = true; +// }, +// setFolderMode(state: Draft, { payload }: PayloadAction) { +// state.isFolderModeEnabled = payload; +// }, +// } as const + +// type BaseWorkspaceReducers = typeof workspaceReducers export const createWorkspaceSlice = < TState extends WorkspaceState, @@ -100,9 +114,10 @@ export const createWorkspaceSlice = < >( name: TName, initialState: TState, - reducers: TReducers, + reducers: ValidateSliceCaseReducers, + extraReducers?: (builder: ActionReducerMapBuilder) => void ) => { - const { actions: editorActions, reducer: editorReducer } = getEditorSlice(initialState.editorState.editorTabs) + const editorReducer = getEditorReducer(initialState.editorState.editorTabs) const subReducer = combineReducers({ editorState: editorReducer, @@ -110,14 +125,48 @@ export const createWorkspaceSlice = < repl: replReducer }) - const { actions, reducer } = createSlice({ + return createSlice({ name, - initialState: initialState, - reducers: { - ...workspaceReducers, - ...reducers, - } as any, + initialState, + reducers, extraReducers: builder => { + builder.addCase(workspaceActions.debugReset,(state) => { + state.isDebugging = false; + state.isRunning = false; + }) + builder.addCase(workspaceActions.debugResume,(state) => { + state.isDebugging = false; + state.isRunning = true; + }) + + builder.addCase(workspaceActions.endClearContext,(state) => { + // TODO Investigate + }) + + builder.addCase(workspaceActions.endDebugPause,(state) => { + state.isDebugging = true; + state.isRunning = false; + }) + + builder.addCase(workspaceActions.endInterruptExecution,(state) => { + // same as debug reset + state.isDebugging = false; + state.isRunning = false; + }) + + builder.addCase(workspaceActions.evalEditor,(state) => { + state.isDebugging = false; + state.isRunning = true; + }) + + builder.addCase(workspaceActions.evalRepl,(state) => { + state.isRunning = true; + }) + + builder.addCase(workspaceActions.setFolderMode,(state, { payload }) => { + state.isFolderModeEnabled = payload; + }) + builder.addCase(replActions.evalInterpreterError, state => { state.isDebugging = false; state.isRunning = false; @@ -131,16 +180,17 @@ export const createWorkspaceSlice = < state.debuggerContext = payload; }); + if (extraReducers) extraReducers(builder) + builder.addDefaultCase((state, action) => { subReducer(state, action) }) } }); - - return { reducer, actions: { - ...editorActions, - ...sideContentActions, - ...replActions, - ...actions, - }} + // return { reducer, actions: { + // ...editorActions, + // ...sideContentActions, + // ...replActions, + // ...actions, + // }} } diff --git a/src/commons/redux/workspace/playground/PlaygroundBase.ts b/src/commons/redux/workspace/playground/PlaygroundBase.ts index d457d39972..62f4a022dd 100644 --- a/src/commons/redux/workspace/playground/PlaygroundBase.ts +++ b/src/commons/redux/workspace/playground/PlaygroundBase.ts @@ -1,4 +1,4 @@ -import { createReducer, Draft, PayloadAction, SliceCaseReducers } from "@reduxjs/toolkit" +import { ActionReducerMapBuilder, createAction, createReducer, SliceCaseReducers, ValidateSliceCaseReducers } from "@reduxjs/toolkit" import { EditorTabState } from "src/commons/workspace/WorkspaceTypes" import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "../WorkspaceRedux" @@ -27,33 +27,76 @@ export const getDefaultPlaygroundState = (initialTabs: EditorTabState[] = []): P usingSubst: false, }) -const basePlaygroundReducers = { - changeStepLimit(state: Draft, { payload }: PayloadAction) { +const playgroundBaseActions = { + changeStepLimit: createAction('playgroundBase/changeStepLimit', (payload: number) => ({ payload })), + toggleUpdateEnv: createAction('playgroundBase/toggleUpdateEnv', (payload: boolean) => ({ payload })), + toggleUsingEnv: createAction('playgroundBase/toggleUsingEnv', (payload: boolean) => ({ payload })), + toggleUsingSubst: createAction('playgroundBase/toggleUsingSubst', (payload: boolean) => ({ payload })), + updateBreakpointSteps: createAction('playgroundBase/updateBreakpointSteps', (payload: number[]) => ({ payload })), + updateEnvSteps: createAction('playgroundBase/updateEnvSteps', (payload: number) => ({ payload })), + updateEnvStepsTotal: createAction('playgroundBase/updateEnvStepsTotal', (payload: number) => ({ payload })) +} as const + +// const basePlaygroundReducers = { +// changeStepLimit(state: Draft, { payload }: PayloadAction) { +// state.stepLimit = payload +// }, +// toggleUpdateEnv(state: Draft, { payload }: PayloadAction) { +// state.updateEnv = payload +// }, +// toggleUsingEnv(state: Draft, { payload }: PayloadAction) { +// state.usingEnv = payload +// }, +// toggleUsingSubst(state: Draft, { payload }: PayloadAction) { +// state.usingSubst = payload +// }, +// updateBreakpointSteps(state: Draft, { payload }: PayloadAction) { +// state.breakpointSteps = payload +// }, +// updateEnvSteps(state: Draft, { payload }: PayloadAction) { +// state.envSteps = payload +// }, +// updateEnvStepsTotal(state: Draft, { payload }: PayloadAction) { +// state.envStepsTotal = payload +// } +// } as const + +const reducerBuilder = (builder: ActionReducerMapBuilder) => { + builder.addCase(playgroundBaseActions.changeStepLimit, (state, { payload }) => { state.stepLimit = payload - }, - toggleUpdateEnv(state: Draft, { payload }: PayloadAction) { + }) + + builder.addCase(playgroundBaseActions.toggleUpdateEnv, (state, { payload }) => { state.updateEnv = payload - }, - toggleUsingEnv(state: Draft, { payload }: PayloadAction) { + }) + + builder.addCase(playgroundBaseActions.toggleUsingEnv, (state, { payload }) => { state.usingEnv = payload - }, - toggleUsingSubst(state: Draft, { payload }: PayloadAction) { + }) + + builder.addCase(playgroundBaseActions.toggleUsingSubst, (state, { payload }) => { state.usingSubst = payload - }, - updateBreakpointSteps(state: Draft, { payload }: PayloadAction) { + }) + + builder.addCase(playgroundBaseActions.updateBreakpointSteps, (state, { payload }) => { state.breakpointSteps = payload - }, - updateEnvSteps(state: Draft, { payload }: PayloadAction) { + }) + + builder.addCase(playgroundBaseActions.updateEnvSteps, (state, { payload }) => { state.envSteps = payload - }, - updateEnvStepsTotal(state: Draft, { payload }: PayloadAction) { + }) + builder.addCase(playgroundBaseActions.updateEnvStepsTotal, (state, { payload }) => { state.envStepsTotal = payload - } -} as const - -export const basePlaygroundReducer = (initialState: T) => createReducer(initialState, basePlaygroundReducers) + }) +} -type PlaygroundBaseReducers = typeof basePlaygroundReducers +export const basePlaygroundReducer = ( + initialState: T, + extraReducers?: (builder: ActionReducerMapBuilder) => void, +) => createReducer(initialState, builder => { + reducerBuilder(builder) + if (extraReducers) extraReducers(builder) +}) export const createPlaygroundSlice = < TState extends PlaygroundWorkspaceState, @@ -62,12 +105,34 @@ export const createPlaygroundSlice = < >( name: TName, initialState: TState, - reducers: TReducers, -) => createWorkspaceSlice( + reducers: ValidateSliceCaseReducers, + extraReducers?: (builder: ActionReducerMapBuilder) => void +) => createWorkspaceSlice( name, initialState, - { - ...basePlaygroundReducers, - ...reducers, + reducers, + builder => { + reducerBuilder(builder) + if (extraReducers) extraReducers(builder) } ) + +// export const createPlaygroundSlice = < +// TState extends PlaygroundWorkspaceState, +// TReducers extends SliceCaseReducers, +// TName extends string = string +// >( +// name: TName, +// initialState: TState, +// reducers: ValidateSliceCaseReducers, +// extraReducers?: (builder: ActionReducerMapBuilder) => void, +// ) => createWorkspaceSlice( +// name, +// initialState, +// reducers, +// builder => { + + +// if (extraReducers) extraReducers(builder) +// } +// ) diff --git a/src/commons/redux/workspace/playground/PlaygroundRedux.ts b/src/commons/redux/workspace/playground/PlaygroundRedux.ts index 55c7b1e393..2982f8d7b7 100644 --- a/src/commons/redux/workspace/playground/PlaygroundRedux.ts +++ b/src/commons/redux/workspace/playground/PlaygroundRedux.ts @@ -36,7 +36,7 @@ export const defaultPlayground: PlaygroundState = { languageConfig: defaultLanguageConfig } -export const { actions: playgroundWorkspaceActions, reducer: playgroundReducer } = createPlaygroundSlice('playground', defaultPlayground, { +const { actions: playgroundWorkspaceActions, reducer } = createPlaygroundSlice('playground', defaultPlayground, { changeQueryString(state, { payload }: PayloadAction) { state.queryString = payload }, @@ -54,6 +54,8 @@ export const { actions: playgroundWorkspaceActions, reducer: playgroundReducer } } }) +export { reducer as playgroundReducer } + export const playgroundActions = { ...playgroundWorkspaceActions, generateLzString: createAction('playground/generateLzString'), @@ -143,7 +145,6 @@ function* updateQueryString() { yield put(playgroundWorkspaceActions.changeQueryString(newQueryString)); } - /** * Gets short url from microservice * @returns {(Response|null)} Response if successful, otherwise null. From f1cf593ac87afb172e09b797d9e7b2cfacdcd7aa Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Fri, 22 Sep 2023 14:12:25 +0800 Subject: [PATCH 06/13] Add redux implementations --- .../application/reducers/RootReducer.ts | 3 +- src/commons/redux/utils.ts | 75 ++ .../redux/workspace/AllWorkspacesRedux.ts | 145 ++-- src/commons/redux/workspace/Hooks.ts | 52 ++ .../redux/workspace/NewWorkspaceSaga.ts | 665 ++++++++++++++++++ src/commons/redux/workspace/SicpRedux.ts | 12 + .../redux/workspace/SourcecastRedux.ts | 122 ++++ .../SourcereelRedux.ts} | 10 +- .../redux/{ => workspace}/StoriesRedux.ts | 8 +- src/commons/redux/workspace/WorkspaceRedux.ts | 156 ++-- .../workspace/assessment/AssessmentBase.ts | 50 ++ .../workspace/assessment/AssessmentRedux.ts | 25 + .../assessment/GithubAssesmentRedux.ts | 17 + .../{ => assessment}/GradingRedux.ts | 12 +- .../workspace/playground/PlaygroundBase.ts | 74 +- .../workspace/playground/PlaygroundRedux.ts | 44 +- .../{ => subReducers}/EditorRedux.ts | 118 +++- .../{ => workspace/subReducers}/ReplRedux.ts | 9 +- .../subReducers}/SideContentRedux.ts | 23 +- src/commons/repl/Repl.tsx | 3 +- src/commons/repl/ReplInput.tsx | 6 +- src/pages/createStore.ts | 35 +- 22 files changed, 1398 insertions(+), 266 deletions(-) create mode 100644 src/commons/redux/utils.ts create mode 100644 src/commons/redux/workspace/Hooks.ts create mode 100644 src/commons/redux/workspace/NewWorkspaceSaga.ts create mode 100644 src/commons/redux/workspace/SicpRedux.ts create mode 100644 src/commons/redux/workspace/SourcecastRedux.ts rename src/commons/redux/{Sourcereel.ts => workspace/SourcereelRedux.ts} (86%) rename src/commons/redux/{ => workspace}/StoriesRedux.ts (93%) create mode 100644 src/commons/redux/workspace/assessment/AssessmentBase.ts create mode 100644 src/commons/redux/workspace/assessment/AssessmentRedux.ts create mode 100644 src/commons/redux/workspace/assessment/GithubAssesmentRedux.ts rename src/commons/redux/workspace/{ => assessment}/GradingRedux.ts (79%) rename src/commons/redux/workspace/{ => subReducers}/EditorRedux.ts (83%) rename src/commons/redux/{ => workspace/subReducers}/ReplRedux.ts (95%) rename src/commons/redux/{ => workspace/subReducers}/SideContentRedux.ts (70%) diff --git a/src/commons/application/reducers/RootReducer.ts b/src/commons/application/reducers/RootReducer.ts index 88c1083947..d24563e998 100644 --- a/src/commons/application/reducers/RootReducer.ts +++ b/src/commons/application/reducers/RootReducer.ts @@ -1,4 +1,6 @@ import { combineReducers } from 'redux'; +// import { WorkspaceReducer as workspaces } from '../../workspace/WorkspaceReducer'; +import { allWorkspacesReducer as workspaces } from 'src/commons/redux/workspace/AllWorkspacesRedux'; import { AcademyReducer as academy } from '../../../features/academy/AcademyReducer'; import { AchievementReducer as achievement } from '../../../features/achievement/AchievementReducer'; @@ -6,7 +8,6 @@ import { DashboardReducer as dashboard } from '../../../features/dashboard/Dashb import { PlaygroundReducer as playground } from '../../../features/playground/PlaygroundReducer'; import { StoriesReducer as stories } from '../../../features/stories/StoriesReducer'; import { FileSystemReducer as fileSystem } from '../../fileSystem/FileSystemReducer'; -import { WorkspaceReducer as workspaces } from '../../workspace/WorkspaceReducer'; import { ApplicationReducer as application } from '../ApplicationReducer'; import { RouterReducer as router } from './CommonsReducer'; import { SessionsReducer as session } from './SessionsReducer'; diff --git a/src/commons/redux/utils.ts b/src/commons/redux/utils.ts new file mode 100644 index 0000000000..b868039eb6 --- /dev/null +++ b/src/commons/redux/utils.ts @@ -0,0 +1,75 @@ +import { ActionCreatorWithOptionalPayload, ActionCreatorWithoutPayload, ActionCreatorWithPreparedPayload, createAction } from "@reduxjs/toolkit"; +import * as Sentry from '@sentry/browser'; +import { SagaIterator } from "redux-saga"; +import { StrictEffect, takeEvery } from "redux-saga/effects"; + +export function createActions< + BaseName extends string, + BaseActions extends Record +>(baseName: BaseName, baseActions: BaseActions) { + return Object.entries(baseActions).reduce((res, [name, func]) => ({ + ...res, + [name]: func + ? createAction(`${baseName}/${name}`, (...args: any) => ({ payload: func(...args) })) + : createAction(`${baseName}/${name}`) + }), {} as { + [K in keyof BaseActions]: K extends string + ? (BaseActions[K] extends (...args: any) => any + ? ActionCreatorWithPreparedPayload< + Parameters, + ReturnType, + K + > + : ActionCreatorWithoutPayload<`${BaseName}/${K}`>) + : never + }) +} + +export function combineSagaHandlers< + TActions extends Record | ActionCreatorWithoutPayload> +>(actions: TActions, handlers: { + [K in keyof TActions]: (action: ReturnType) => SagaIterator +}, others?: (takeEvery: typeof saferTakeEvery) => SagaIterator): () => SagaIterator { + const sagaHandlers = Object.values(handlers).map(([actionName, saga]) => saferTakeEvery(actions[actionName].type, saga)) + return function*(): SagaIterator { + yield* sagaHandlers + if (others) { + const obj = others(saferTakeEvery) + while (true) { + const { done, value } = obj.next() + if (done) break + yield value + } + } + } +} + +function handleUncaughtError(error: any) { + if (process.env.NODE_ENV === 'development') { + // react-error-overlay is a "special" package that's automatically included + // in development mode by CRA + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + import('react-error-overlay').then(reo => reo.reportRuntimeError(error)); + } + Sentry.captureException(error); + console.error(error); +} + +export function saferTakeEvery< + Action extends ActionCreatorWithOptionalPayload | ActionCreatorWithPreparedPayload, +>( + actionPattern: Action, + fn: (action: ReturnType) => Generator> +) { + function* wrapper(action: ReturnType) { + try { + yield* fn(action) + } catch (error) { + handleUncaughtError(error); + } + } + + return takeEvery(actionPattern.type, wrapper) +} diff --git a/src/commons/redux/workspace/AllWorkspacesRedux.ts b/src/commons/redux/workspace/AllWorkspacesRedux.ts index 224a1a92a9..3f01567fbc 100644 --- a/src/commons/redux/workspace/AllWorkspacesRedux.ts +++ b/src/commons/redux/workspace/AllWorkspacesRedux.ts @@ -1,24 +1,38 @@ -import { Action, ActionCreatorWithPreparedPayload, combineReducers, createAction, createReducer, PayloadAction, PayloadActionCreator } from "@reduxjs/toolkit"; - -import { replActions } from "../ReplRedux"; -import { sideContentActions, SideContentLocation } from "../SideContentRedux"; -import { defaultStories, getDefaultStoriesEnv, storiesReducer, StoriesState } from "../StoriesRedux"; -import { editorActions } from "./EditorRedux"; -import { defaultGradingState, gradingReducer, GradingWorkspaceState } from "./GradingRedux"; -import { basePlaygroundReducer } from "./playground/PlaygroundBase"; +import { Action, ActionCreatorWithPreparedPayload, combineReducers, createReducer, PayloadAction, PayloadActionCreator } from "@reduxjs/toolkit"; +import { LOG_OUT } from "src/commons/application/types/CommonsTypes"; + +import { assessmentActions } from "./assessment/AssessmentBase"; +import { assessmentReducer, AssessmentWorkspaceState, defaultAssessment } from "./assessment/AssessmentRedux"; +import { defaultGithubAssessment, githubAssessmentReducer, GitHubAssessmentWorkspaceState } from "./assessment/GithubAssesmentRedux"; +import { defaultGradingState, gradingReducer, GradingWorkspaceState } from "./assessment/GradingRedux"; +import { basePlaygroundReducer, playgroundBaseActions } from "./playground/PlaygroundBase"; import { defaultPlayground, playgroundReducer, PlaygroundState } from "./playground/PlaygroundRedux"; -import { workspaceActions } from "./WorkspaceRedux"; - -type WorkspaceManagerState = { +import { defaultSicp, sicpReducer, SicpWorkspaceState } from "./SicpRedux"; +import { defaultSourcecast, sourcecastReducer, SourcecastWorkspaceState } from "./SourcecastRedux"; +import { defaultSourcereel, sourcereelReducer, SourcereelWorkspaceState } from "./SourcereelRedux"; +import { defaultStories, getDefaultStoriesEnv, storiesReducer, StoriesState } from "./StoriesRedux"; +import { editorActions } from "./subReducers/EditorRedux"; +import { replActions } from "./subReducers/ReplRedux"; +import { NonStoryWorkspaceLocation, sideContentActions, SideContentLocation } from "./subReducers/SideContentRedux"; +import { isNonStoryWorkspaceLocation, workspaceActions } from "./WorkspaceRedux"; + +export type WorkspaceManagerState = { + assessment: AssessmentWorkspaceState + githubAssessment: GitHubAssessmentWorkspaceState grading: GradingWorkspaceState, playground: PlaygroundState stories: StoriesState + sicp: SicpWorkspaceState + sourcecast: SourcecastWorkspaceState + sourcereel: SourcereelWorkspaceState } const commonWorkspaceActionsInternal = { + ...assessmentActions, ...editorActions, ...replActions, ...sideContentActions, + ...playgroundBaseActions, ...workspaceActions, } @@ -32,7 +46,7 @@ type CommonWorkspaceActions = { type CommonWorkspaceAction = PayloadAction<{ payload: T, location: SideContentLocation }> -const commonWorkspaceActions = Object.entries(commonWorkspaceActionsInternal).reduce((res, [name, creator]) => ({ +export const allWorkspaceActions = Object.entries(commonWorkspaceActionsInternal).reduce((res, [name, creator]) => ({ ...res, [name]: (location: SideContentLocation, ...args: any) => { // @ts-ignore @@ -47,84 +61,93 @@ const commonWorkspaceActions = Object.entries(commonWorkspaceActionsInternal).re } }), {} as CommonWorkspaceActions) -const commonWorkspaceActionTypes = Object.keys(commonWorkspaceActions) - -export const allWorkspaceActions = { - ...commonWorkspaceActions, - logOut: createAction('workspaces/logOut') -} - const allWorkspaceReducers = { + assessment: assessmentReducer, + githubAssessment: githubAssessmentReducer, grading: gradingReducer, playground: playgroundReducer, + sicp: sicpReducer, + sourcecast: sourcecastReducer, + sourcereel: sourcereelReducer, stories: storiesReducer, } +export const getWorkspace = < + T extends Record & { stories: { envs: Record }}, + TLoc extends SideContentLocation +>( + source: T, location: TLoc +): TLoc extends NonStoryWorkspaceLocation ? T[TLoc] : T['stories']['envs'][TLoc] => { + if (isNonStoryWorkspaceLocation(location)) { + const result = source[location] + return result + } + + const [, storyEnv] = location.split('.') + return source.stories.envs[storyEnv] +} + const workspaceManagerReducer = combineReducers(allWorkspaceReducers) -const defaultWorkspaceManager: WorkspaceManagerState = { +export const defaultWorkspaceManager: WorkspaceManagerState = { + assessment: defaultAssessment, + githubAssessment: defaultGithubAssessment, grading: defaultGradingState, playground: defaultPlayground, + sicp: defaultSicp, + sourcecast: defaultSourcecast, + sourcereel: defaultSourcereel, stories: defaultStories, } +export function getWorkspaceReducer(location: T): (typeof allWorkspaceReducers)[T] { + return allWorkspaceReducers[location] +} + +const commonWorkspaceActionTypes = Object.keys(allWorkspaceActions) const isCommonWorkspaceAction = (action: Action): action is CommonWorkspaceAction => commonWorkspaceActionTypes.includes[action.type] export const allWorkspacesReducer = createReducer(defaultWorkspaceManager, builder => { + builder.addCase(LOG_OUT, (state) => { + // Preserve the playground workspace even after log out + const playground = state.playground + return ({ + ...defaultWorkspaceManager, + playground, + }); + }) + builder.addMatcher(isCommonWorkspaceAction, (state, { payload: { payload, location }, ...action }) => { const newAction = { ...action, payload, } - if (location.startsWith('stories')) { + if (!isNonStoryWorkspaceLocation(location)) { const [, storyEnv] = location.split('.') const storyReducer = basePlaygroundReducer(getDefaultStoriesEnv(storyEnv)) - state.stories.envs[storyEnv] = storyReducer(state.stories[storyEnv], action) - } else { - state[location] = allWorkspaceReducers[location](state[location], newAction) - } - }) - builder.addDefaultCase((state, action) => workspaceManagerReducer(state as WorkspaceManagerState, action)) -}) - -/* -export function allWorkspacesReducer(state: WorkspaceManagerState, action: SourceActionType) { - let workspaceLocation: SideContentLocation - if ((action as any).location) { - workspaceLocation = (action as any).location - } else { - workspaceLocation = 'assessment' - } - switch(action.type) { - default: { - const newAction = { - ...action, - payload: (action as any).payload - } - - if (workspaceLocation.startsWith('stories')) { - const [, storyEnv] = workspaceLocation.split('.') - const storyReducer = basePlaygroundReducer(getDefaultStoriesEnv(storyEnv)) - - return { - ...state, - stories: { - ...state.stories, - envs: { - ...state.stories.envs, - [storyEnv]: storyReducer(state.stories[storyEnv], action) - } + return { + ...state, + stories: { + ...state.stories, + envs: { + ...state.stories.envs, + [storyEnv]: storyReducer(state.stories[storyEnv], action) } - } + } } + } else { + const workspace = getWorkspace(state, location) + const reducer = getWorkspaceReducer(location) return { ...state, - [workspaceLocation]: allWorkspaceReducers[workspaceLocation](state[workspaceLocation], newAction) + [location]: reducer(workspace as any, newAction) } + // state[location] = allWorkspaceReducers[location](workspace, newAction) } - } -} -*/ + }) + + builder.addDefaultCase((state, action) => workspaceManagerReducer(state as WorkspaceManagerState, action)) +}) diff --git a/src/commons/redux/workspace/Hooks.ts b/src/commons/redux/workspace/Hooks.ts new file mode 100644 index 0000000000..af151df74c --- /dev/null +++ b/src/commons/redux/workspace/Hooks.ts @@ -0,0 +1,52 @@ +import { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { SideContentType } from "src/commons/sideContent/SideContentTypes"; +import { useTypedSelector } from "src/commons/utils/Hooks"; + +import { WorkspaceManagerState } from "./AllWorkspacesRedux"; +import { StoriesEnvState } from "./StoriesRedux"; +import { EditorState } from "./subReducers/EditorRedux"; +import { NonStoryWorkspaceLocation, sideContentActions,SideContentLocation, SideContentState, StoryWorkspaceLocation } from "./subReducers/SideContentRedux"; + +const getLocation = (location: SideContentLocation) => { + if (location.startsWith('stories')) { + return location.split('.') + } + return [location] +} + +export function useWorkspace(location: T): WorkspaceManagerState[T] +export function useWorkspace(location: StoryWorkspaceLocation): StoriesEnvState +export function useWorkspace(location: SideContentLocation) { + return useTypedSelector(state => { + const [workspaceLocation, storyEnv] = getLocation(location) + if (workspaceLocation === 'stories') { + return state.workspaces.stories.envs[storyEnv] + } + + return state.workspaces[workspaceLocation] + }) +} + +export function useEditorState(location: T): EditorState +export function useEditorState(location: StoryWorkspaceLocation): EditorState +export function useEditorState(location: SideContentLocation) { + return useWorkspace(location as any).editorState; +} +export const useRepl = (location: SideContentLocation) => useWorkspace(location as any).repl + +export const useSideContent = (location: SideContentLocation, defaultTab: SideContentType) => { + const dispatch = useDispatch() + const sideContent: SideContentState = useWorkspace(location as any).sideContent + const currentTab = sideContent.selectedTabId + + const setSelectedTab = useCallback((newId: SideContentType) => { + dispatch(sideContentActions.visitSideContent(newId)) + }, [dispatch]) + + return { + ...sideContent, + selectedTab: currentTab ?? defaultTab, + setSelectedTab, + } +} diff --git a/src/commons/redux/workspace/NewWorkspaceSaga.ts b/src/commons/redux/workspace/NewWorkspaceSaga.ts new file mode 100644 index 0000000000..69be37d7a5 --- /dev/null +++ b/src/commons/redux/workspace/NewWorkspaceSaga.ts @@ -0,0 +1,665 @@ +import { FSModule } from "browserfs/dist/node/core/FS"; +import { Context, interrupt, parseError, Result, resume, runFilesInContext } from "js-slang"; +import { defineSymbol } from "js-slang/dist/createContext"; +import { InterruptedError } from "js-slang/dist/errors/errors"; +import { parse } from "js-slang/dist/parser/parser"; +import { typeCheck } from "js-slang/dist/typeChecker/typeChecker"; +import { Chapter, SourceError, Variant } from "js-slang/dist/types"; +import { validateAndAnnotate } from "js-slang/dist/validator/validator"; +import { posix as pathlib } from 'path'; +import { SagaIterator } from "redux-saga"; +import { call, put, race, select, StrictEffect, take } from "redux-saga/effects"; +import * as Sourceror from 'sourceror'; +import { defaultEditorValue,isSourceLanguage,OverallState } from "src/commons/application/ApplicationTypes"; +import { writeFileRecursively } from "src/commons/fileSystem/utils"; +import { SideContentType } from "src/commons/sideContent/SideContentTypes"; +import { actions } from "src/commons/utils/ActionsHelper"; +import DisplayBufferService from "src/commons/utils/DisplayBufferService"; +import { getBlockExtraMethodsString, getDifferenceInMethods, getStoreExtraMethodsString, makeElevatedContext, visualizeEnv } from "src/commons/utils/JsSlangHelper"; +import { showWarningMessage } from "src/commons/utils/notifications/NotificationsHelper"; +import { makeExternalBuiltins as makeSourcerorExternalBuiltins } from "src/commons/utils/SourcerorHelper"; +import { EditorTabState,EVAL_SILENT } from "src/commons/workspace/WorkspaceTypes"; +import { EventType } from "src/features/achievement/AchievementTypes"; +import { DeviceSession } from "src/features/remoteExecution/RemoteExecutionTypes"; +import { getWorkspaceBasePath, WORKSPACE_BASE_PATHS } from "src/pages/fileSystem/createInBrowserFileSystem"; + +import { allWorkspaceActions } from "./AllWorkspacesRedux"; +import { PlaygroundWorkspaces,PlaygroundWorkspaceState } from "./playground/PlaygroundBase"; +import { SideContentLocation } from "./subReducers/SideContentRedux"; +import { getWorkspaceSelector,WorkspaceState } from "./WorkspaceRedux"; + +function* updateInspector(workspaceLocation: SideContentLocation): SagaIterator { + const workspaceSelector = getWorkspaceSelector(workspaceLocation) + const workspace: WorkspaceState = yield select((state: OverallState) => workspaceSelector(state.workspaces)) + const activeEditorTabIndex = workspace.editorState.activeEditorTabIndex! + + try { + const row = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1; + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + + yield put(allWorkspaceActions.updateEditorHighlightedLines(workspaceLocation, activeEditorTabIndex, [])); + // We highlight only one row to show the current line + // If we highlight from start to end, the whole program block will be highlighted at the start + // since the first node is the program node + yield put(allWorkspaceActions.updateEditorHighlightedLines(workspaceLocation, activeEditorTabIndex, [[row, row]])); + yield call(visualizeEnv, lastDebuggerResult) + // visualizeEnv(lastDebuggerResult); + } catch (e) { + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + yield put(allWorkspaceActions.updateEditorHighlightedLines(workspaceLocation, activeEditorTabIndex, [])); + // most likely harmless, we can pretty much ignore this. + // half of the time this comes from execution ending or a stack overflow and + // the context goes missing. + } +} + +/** + * Inserts debugger statements into the code based off the breakpoints set by the user. + * + * For every breakpoint, a corresponding `debugger;` statement is inserted at the start + * of the line that the breakpoint is placed at. The `debugger;` statement is available + * in both JavaScript and Source, and invokes any available debugging functionality. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger + * for more information. + * + * While it is typically the case that statements are contained within a single line, + * this is not necessarily true. For example, the code `const x = 3;` can be rewritten as: + * ``` + * const x + * = 3; + * ``` + * A breakpoint on the line `= 3;` would thus result in a `debugger;` statement being + * added in the middle of another statement. The resulting code would then be syntactically + * invalid. + * + * To work around this issue, we parse the code to check for syntax errors whenever we + * add a `debugger;` statement. If the addition of a `debugger;` statement results in + * invalid code, an error message is outputted with the line number of the offending + * breakpoint. + * + * @param workspaceLocation The location of the current workspace. + * @param code The code which debugger statements should be inserted into. + * @param breakpoints The breakpoints corresponding to the code. + * @param context The context in which the code should be evaluated in. + */ +function* insertDebuggerStatements( + location: SideContentLocation, + code: string, + breakpoints: string[], + context: Context +): Generator { + // Check for initial syntax errors. + if (isSourceLanguage(context.chapter)) { + parse(code, context); + } + + // If there are syntax errors, we do not insert the debugger statements. + // Instead, we let the code be evaluated so that the error messages are printed. + if (context.errors.length > 0) { + context.errors = []; + return code; + } + + // Otherwise, we step through the breakpoints one by one & try to insert + // corresponding debugger statements. + const lines = code.split('\n'); + let transformedCode = code; + for (let i = 0; i < breakpoints.length; i++) { + if (!breakpoints[i]) continue; + lines[i] = 'debugger;' + lines[i]; + // Reconstruct the code & check that the code is still syntactically valid. + // The insertion of the debugger statement is potentially invalid if it + // happens within an existing statement (that is split across lines). + transformedCode = lines.join('\n'); + if (isSourceLanguage(context.chapter)) { + parse(transformedCode, context); + } + // If the resulting code is no longer syntactically valid, throw an error. + if (context.errors.length > 0) { + const errorMessage = `Hint: Misplaced breakpoint at line ${i + 1}.`; + yield put(allWorkspaceActions.sendReplInputToOutput(location, errorMessage)); + return code; + } + } + + /* + Not sure how this works, but there were some issues with breakpoints + I'm not sure why `in` is being used here, given that it's usually not + the intended effect + + for (const breakpoint in breakpoints) { + // Add a debugger statement to the line with the breakpoint. + const breakpointLineNum: number = parseInt(breakpoint); + lines[breakpointLineNum] = 'debugger;' + lines[breakpointLineNum]; + // Reconstruct the code & check that the code is still syntactically valid. + // The insertion of the debugger statement is potentially invalid if it + // happens within an existing statement (that is split across lines). + transformedCode = lines.join('\n'); + if (isSourceLanguage(context.chapter)) { + parse(transformedCode, context); + } + // If the resulting code is no longer syntactically valid, throw an error. + if (context.errors.length > 0) { + const errorMessage = `Hint: Misplaced breakpoint at line ${breakpointLineNum + 1}.`; + yield put(actions.sendReplInputToOutput(errorMessage, workspaceLocation)); + return code; + } + } + */ + + // Finally, return the transformed code with debugger statements added. + return transformedCode; +} + +export function* blockExtraMethods( + elevatedContext: Context, + context: Context, + execTime: number, + location: SideContentLocation, + unblockKey?: string +) { + // Extract additional methods available in the elevated context relative to the context + const toBeBlocked = getDifferenceInMethods(elevatedContext, context); + if (unblockKey) { + const storeValues = getStoreExtraMethodsString(toBeBlocked, unblockKey); + const storeValuesFilePath = '/storeValues.js'; + const storeValuesFiles = { + [storeValuesFilePath]: storeValues + }; + yield call( + evalCode, + storeValuesFiles, + storeValuesFilePath, + elevatedContext, + execTime, + location, + EVAL_SILENT + ); + } + + const nullifier = getBlockExtraMethodsString(toBeBlocked); + const nullifierFilePath = '/nullifier.js'; + const nullifierFiles = { + [nullifierFilePath]: nullifier + }; + yield call( + evalCode, + nullifierFiles, + nullifierFilePath, + elevatedContext, + execTime, + location, + EVAL_SILENT + ); +} + +export default function* WorkspaceSaga(): SagaIterator { + yield saferTakeEvery(allWorkspaceActions.toggleFolderMode, function* ({ payload: { location }}) { + const selector = getWorkspaceSelector(location) + const isFolderModeEnabled: boolean = yield select((state: OverallState) => selector(state.workspaces)) + + yield put(allWorkspaceActions.setFolderMode(location, !isFolderModeEnabled)); + const warningMessage = `Folder mode ${!isFolderModeEnabled ? 'enabled' : 'disabled'}`; + yield call(showWarningMessage, warningMessage, 750); + }) + + yield saferTakeEvery(allWorkspaceActions.setFolderMode, function* ({ payload: { location }}) { + const workspaceSelector = getWorkspaceSelector(location) + const isFolderModeEnabled: boolean = yield select( + (state: OverallState) => workspaceSelector(state.workspaces).isFolderModeEnabled + ); + // Do nothing if Folder mode is enabled. + if (isFolderModeEnabled) { + return; + } + + const editorTabs: EditorTabState[] = yield select( + (state: OverallState) => workspaceSelector(state.workspaces).editorState.editorTabs + ); + // If Folder mode is disabled and there are no open editor tabs, add an editor tab. + if (editorTabs.length === 0) { + const defaultFilePath = `${WORKSPACE_BASE_PATHS[location]}/program.js`; + const fileSystem: FSModule | null = yield select( + (state: OverallState) => state.fileSystem.inBrowserFileSystem + ); + // If the file system is not initialised, add an editor tab with the default editor value. + if (fileSystem === null) { + yield put(allWorkspaceActions.addEditorTab(location, defaultFilePath, defaultEditorValue)); + return; + } + const editorValue: string = yield new Promise((resolve, reject) => { + fileSystem.exists(defaultFilePath, fileExists => { + if (!fileExists) { + // If the file does not exist, we need to also create it in the file system. + writeFileRecursively(fileSystem, defaultFilePath, defaultEditorValue) + .then(() => resolve(defaultEditorValue)) + .catch(err => reject(err)); + return; + } + fileSystem.readFile(defaultFilePath, 'utf-8', (err, fileContents) => { + if (err) { + reject(err); + return; + } + if (fileContents === undefined) { + reject(new Error('File exists but has no contents.')); + return; + } + resolve(fileContents); + }); + }); + }); + yield put(allWorkspaceActions.addEditorTab(location, defaultFilePath, editorValue)); + } + }) + + // Mirror editor updates to the associated file in the filesystem. + yield saferTakeEvery(allWorkspaceActions.updateEditorValue, function* ({ payload: { location, payload: { + editorTabIndex, newEditorValue } }}) { + const workspaceSelector = getWorkspaceSelector(location) + + const filePath: string | undefined = yield select( + (state: OverallState) => + workspaceSelector(state.workspaces).editorState.editorTabs[editorTabIndex].filePath + ); + // If the code does not have an associated file, do nothing. + if (filePath === undefined) { + return; + } + + const fileSystem: FSModule | null = yield select( + (state: OverallState) => state.fileSystem.inBrowserFileSystem + ); + // If the file system is not initialised, do nothing. + if (fileSystem === null) { + return; + } + + fileSystem.writeFile(filePath, newEditorValue, err => { + if (err) { + console.error(err); + } + }); + }) +} + +function* clearContext(location: SideContentLocation, code: string) { + const workspaceSelector = getWorkspaceSelector(location) + const workspace: WorkspaceState = yield select((state: OverallState) => workspaceSelector(state.workspaces)) + + yield put(allWorkspaceActions.beginClearContext(location, + workspace.context.chapter, + workspace.context.variant, + workspace.globals, + workspace.context.externalSymbols + )) + + yield take(allWorkspaceActions.endClearContext) + defineSymbol(workspace.context, '__PROGRAM__', code) +} + +function retrieveFilesInWorkspaceAsRecord( + location: SideContentLocation, + fileSystem: FSModule +) { + const workspaceBasePath = getWorkspaceBasePath(location) + const files: Record = {} + const processDirectory = (path: string) => new Promise((resolve, reject) => fileSystem.readdir(path, (err, fileNames) => { + if (err) { + reject(err) + return + } + + if (fileNames) { + Promise.all(fileNames.map(fileName => { + const fullPath = pathlib.join(path, fileName) + return new Promise((resolve, reject) => fileSystem.lstat(fullPath, (err, stats) => { + if (err) { + reject(err) + return + } + + if (stats !== undefined) { + if (stats.isFile()) { + fileSystem.readFile(fullPath, 'utf-8', (err, contents) => { + if (err) { + reject(err) + return + } + + if (contents === undefined) { + // TODO check this + reject('some err here') + return + } + + files[fullPath] = contents + resolve() + }) + } else if (stats.isDirectory()) { + processDirectory(fullPath) + .then(resolve) + .catch(reject) + } + reject('Should never get here!') + } else { + resolve() + } + })) + } + )).then(() => resolve()) + } + })) + return processDirectory(workspaceBasePath) +} + +export function* evalEditor( + location: SideContentLocation +) { + const workspaceSelector = getWorkspaceSelector(location) + const workspace: WorkspaceState = yield select((state: OverallState) => workspaceSelector(state.workspaces)) + const fileSystem: FSModule = yield select((state: OverallState) => state.fileSystem.inBrowserFileSystem) + + const activeEditorTabIndex = workspace.editorState.activeEditorTabIndex + if (activeEditorTabIndex === null) { + throw new Error('Cannot evaluate program without an entrypoint file.'); + } + + const defaultFilePath = `${getWorkspaceBasePath(location)}/program.js` + let files: Record; + if (workspace.isFolderModeEnabled) { + files = yield call(retrieveFilesInWorkspaceAsRecord, location, fileSystem); + } else { + files = { + [defaultFilePath]: workspace.editorState.editorTabs[activeEditorTabIndex].value + }; + } + + yield put(actions.addEvent([EventType.RUN_CODE])) + + const entrypointFilePath = workspace.editorState.editorTabs[activeEditorTabIndex].filePath ?? defaultFilePath; + const remoteExecutionSession: DeviceSession | undefined = yield select((state: OverallState) => state.session.remoteExecutionSession) + + if (remoteExecutionSession && remoteExecutionSession.workspace === location) { + yield put(actions.remoteExecRun(files, entrypointFilePath)); + } else { + // End any code that is running right now. + yield put(allWorkspaceActions.beginInterruptExecution(location)); + const entrypointCode = files[entrypointFilePath]; + yield* clearContext(location, entrypointCode); + yield put(allWorkspaceActions.clearReplOutput(location)); + + + // Insert debugger statements at the lines of the program with a breakpoint. + for (const editorTab of workspace.editorState.editorTabs) { + const filePath = editorTab.filePath ?? defaultFilePath; + const code = editorTab.value; + const breakpoints = editorTab.breakpoints; + files[filePath] = yield* insertDebuggerStatements( + location, + code, + breakpoints, + workspace.context + ); + } + + // Evaluate the prepend silently with a privileged context, if it exists + if (workspace.programPrependValue.length) { + const elevatedContext = makeElevatedContext(workspace.context); + const prependFilePath = '/prepend.js'; + const prependFiles = { + [prependFilePath]: workspace.programPrependValue + }; + yield call( + evalCode, + prependFiles, + prependFilePath, + elevatedContext, + workspace.execTime, + location, + EVAL_SILENT + ); + // Block use of methods from privileged context + yield* blockExtraMethods(elevatedContext, workspace.context, workspace.execTime, location); + } + + yield call( + evalCode, + files, + entrypointFilePath, + workspace.context, + workspace.execTime, + location, + allWorkspaceActions.evalEditor.type, + ); + } +} + +function* evalWithEnvOrSubst( + files: Record, + entrypointFilePath: string, + context: Context, + location: PlaygroundWorkspaces +) { + const workspaceSelector = getWorkspaceSelector(location) + const workspace: PlaygroundWorkspaceState = yield select((state: OverallState) => workspaceSelector(state.workspaces)) + + const substActiveAndCorrectChapter = workspace.sideContent.selectedTabId === SideContentType.substVisualizer && context.chapter <= Chapter.SOURCE_2 + const envActiveAndCorrectChapter = workspace.sideContent.selectedTabId === SideContentType.envVisualizer && context.chapter >= Chapter.SOURCE_3 + + yield call( + runFilesInContext, + files, + entrypointFilePath, + context, + { + executionMethod: envActiveAndCorrectChapter ? 'ec-evaluator' : 'auto', + envSteps: workspace.envSteps, + originalMaxExecTime: workspace.execTime, + stepLimit: workspace.stepLimit, + throwInfiniteLoops: true, + useSubst: substActiveAndCorrectChapter, + } + ) +} + +export function* evalCode( + files: Record, + entrypointFilePath: string, + context: Context, + execTime: number, + location: SideContentLocation, + actionType: string +): SagaIterator { + context.runtime.debuggerOn = + (actionType === allWorkspaceActions.evalEditor.type || actionType === allWorkspaceActions.debugResume.type) && context.chapter > 2; + + function isPlaygroundWorkspace(location: SideContentLocation): location is PlaygroundWorkspaces { + return location === 'playground' || location === 'sicp' || location.startsWith('stories') + } + + const workspaceSelector = getWorkspaceSelector(location) + const workspace: WorkspaceState = yield select((state: OverallState) => workspaceSelector(state.workspaces)) + + if (isPlaygroundWorkspace(location)) { + yield* evalWithEnvOrSubst(files, entrypointFilePath, context, location) + } + + const isNonDet: boolean = context.variant === Variant.NON_DET; + const isLazy: boolean = context.variant === Variant.LAZY; + const isWasm: boolean = context.variant === Variant.WASM; + + + // handle env visualizer and subst visualizers + const substActiveAndCorrectChapter = workspace.sideContent.selectedTabId === SideContentType.substVisualizer && context.chapter <= Chapter.SOURCE_2 + const envActiveAndCorrectChapter = workspace.sideContent.selectedTabId === SideContentType.envVisualizer && context.chapter >= Chapter.SOURCE_3 + + const entrypointCode = files[entrypointFilePath] + async function wasm_compile_and_run( + wasmCode: string, + wasmContext: Context, + isRepl: boolean + ): Promise { + return Sourceror.compile(wasmCode, wasmContext, isRepl) + .then((wasmModule: WebAssembly.Module) => { + const transcoder = new Sourceror.Transcoder(); + return Sourceror.run( + wasmModule, + Sourceror.makePlatformImports(makeSourcerorExternalBuiltins(wasmContext), transcoder), + transcoder, + wasmContext, + isRepl + ); + }) + .then( + (returnedValue: any): Result => ({ status: 'finished', context, value: returnedValue }), + (e: any): Result => { + console.log(e); + return { status: 'error' }; + } + ); + } + function call_variant(variant: Variant) { + if (variant === Variant.NON_DET) { + return entrypointCode.trim() === TRY_AGAIN + ? call(resume, lastNonDetResult) + : call(runFilesInContext, files, entrypointFilePath, context, { + executionMethod: 'interpreter', + originalMaxExecTime: execTime, + stepLimit: workspace.stepLimit, + useSubst: substActiveAndCorrectChapter, + envSteps: workspace.envSteps + }); + } else if (variant === Variant.LAZY) { + return call(runFilesInContext, files, entrypointFilePath, context, { + scheduler: 'preemptive', + originalMaxExecTime: execTime, + stepLimit: stepLimit, + useSubst: substActiveAndCorrectChapter, + envSteps: envSteps + }); + } else if (variant === Variant.WASM) { + // Note: WASM does not support multiple file programs. + return call(wasm_compile_and_run, entrypointCode, context, actionType === EVAL_REPL); + } else { + throw new Error('Unknown variant: ' + variant); + } + } + + + const { result, interrupted, paused } = yield race({ + result: + actionType === allWorkspaceActions.debugResume.type + ? call(resume, lastDebuggerResult) + : isNonDet || isLazy || isWasm + ? call_variant(context.variant) + : call(runFilesInContext, files, entrypointFilePath, context, { + scheduler: 'preemptive', + originalMaxExecTime: execTime, + stepLimit: stepLimit, + throwInfiniteLoops: true, + useSubst: substActiveAndCorrectChapter, + envSteps: envSteps + }), + + /** + * A BEGIN_INTERRUPT_EXECUTION signals the beginning of an interruption, + * i.e the trigger for the interpreter to interrupt execution. + */ + interrupted: take(allWorkspaceActions.beginInterruptExecution), + paused: take(allWorkspaceActions.beginDebugPause) + }) + + if (interrupted) { + interrupt(context); + /* Redundancy, added ensure that interruption results in an error. */ + context.errors.push(new InterruptedError(context.runtime.nodes[0])); + yield put(allWorkspaceActions.debugReset(location)); + yield put(allWorkspaceActions.endInterruptExecution(location)); + yield call(showWarningMessage, 'Execution aborted', 750); + return; + } + + if (paused) { + yield put(allWorkspaceActions.endDebugPause(location)); + lastDebuggerResult = manualToggleDebugger(context); + yield call(updateInspector, location); + yield call(showWarningMessage, 'Execution paused', 750); + return; + } + + if ( + result.status !== 'suspended' && + result.status !== 'finished' && + result.status !== 'suspended-non-det' && + result.status !== 'suspended-ec-eval' + ) { + yield put(allWorkspaceActions.handleConsoleLog(location, DisplayBufferService.dump())) + yield put(allWorkspaceActions.evalInterpreterError(location, context.errors)) + + // we need to parse again, but preserve the errors in context + const oldErrors = context.errors; + context.errors = []; + // Note: Type checking does not support multiple file programs. + const parsed = yield call(parse, entrypointCode, context) + let typeErrors: SourceError[] = [] + if (parsed) { + const validatedProgram = yield call(validateAndAnnotate, parsed, context) + ;([, typeErrors] = yield call(typeCheck, validatedProgram, context)) + } + + context.errors = oldErrors; + // for achievement event tracking + const events = context.errors.length > 0 ? [EventType.ERROR] : []; + if (typeErrors.length > 0) { + events.push(EventType.ERROR) + yield put(allWorkspaceActions.sendReplInputToOutput(location, `Hints:\n${parseError(typeErrors)}`)) + } + + yield put(actions.addEvent(events)); + return; + } else if (result.status === 'suspended' || result.status === 'suspended-ec-eval') { + yield put(allWorkspaceActions.endDebugPause(location)); + yield put(allWorkspaceActions.evalInterpreterSuccess(location, 'Breakpoint hit!')); + return; + } else if (isNonDet) { + if (result.value === 'cut') { + result.value = undefined; + } + lastNonDetResult = result; + } + + yield put(allWorkspaceActions.handleConsoleLog(location, DisplayBufferService.dump())) + + // Do not write interpreter output to REPL, if executing chunks (e.g. prepend/postpend blocks) + if (actionType !== EVAL_SILENT) { + yield put(allWorkspaceActions.evalInterpreterSuccess(location, result.value)) + } + + if ( + actionType === allWorkspaceActions.evalEditor.type || + actionType === allWorkspaceActions.evalRepl.type || + actionType === allWorkspaceActions.debugResume.type + ) { + if (context.errors.length > 0) { + yield put(actions.addEvent([EventType.ERROR])) + } + yield put(allWorkspaceActions.notifyProgramEvaluated( + location, + result, + lastDebuggerResult, + entrypointCode, + context + )) + } + + // The first time the code is executed using the explicit control evaluator, + // the total number of steps and the breakpoints are updated in the Environment Visualiser slider. + if (context.executionMethod === 'ec-evaluator' && needUpdateEnv) { + yield put(allWorkspaceActions.updateEnvStepsTotal(location, context.runtime.envStepsTotal)); + yield put(allWorkspaceActions.toggleUpdateEnv(location, false)); + yield put(allWorkspaceActions.updateBreakpointSteps(location, context.runtime.breakpointSteps)); + } + + // we need to stop the intro icon from flashing? +} diff --git a/src/commons/redux/workspace/SicpRedux.ts b/src/commons/redux/workspace/SicpRedux.ts new file mode 100644 index 0000000000..85041f9561 --- /dev/null +++ b/src/commons/redux/workspace/SicpRedux.ts @@ -0,0 +1,12 @@ +import { defaultEditorValue, getDefaultFilePath } from "../../application/ApplicationTypes"; +import { createPlaygroundSlice, getDefaultPlaygroundState,PlaygroundWorkspaceState } from "./playground/PlaygroundBase"; + +export type SicpWorkspaceState = PlaygroundWorkspaceState +export const defaultSicp: SicpWorkspaceState = getDefaultPlaygroundState([{ + filePath: getDefaultFilePath('sicp'), + value: defaultEditorValue, + highlightedLines: [], + breakpoints: [] +}]) + +export const { reducer: sicpReducer } = createPlaygroundSlice('sicp', defaultSicp, {}) diff --git a/src/commons/redux/workspace/SourcecastRedux.ts b/src/commons/redux/workspace/SourcecastRedux.ts new file mode 100644 index 0000000000..2673f38170 --- /dev/null +++ b/src/commons/redux/workspace/SourcecastRedux.ts @@ -0,0 +1,122 @@ +import { PayloadAction } from "@reduxjs/toolkit"; +import { Chapter } from "js-slang/dist/types"; +import { CodeDelta, Input, PlaybackData, PlaybackStatus, SourcecastData } from "src/features/sourceRecorder/SourceRecorderTypes"; + +import { ExternalLibraryName } from "../../application/types/ExternalTypes"; +import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "./WorkspaceRedux"; + +export type SourcecastWorkspaceState = WorkspaceState & { + readonly audioUrl: string; + readonly codeDeltasToApply: CodeDelta[] | null; + readonly currentPlayerTime: number; + readonly description: string | null; + readonly inputToApply: Input | null; + readonly playbackData: PlaybackData; + readonly playbackDuration: number; + readonly playbackStatus: PlaybackStatus; + readonly sourcecastIndex: SourcecastData[] | null; + readonly title: string | null; + readonly uid: string | null; +} + +export const defaultSourcecast: SourcecastWorkspaceState = { + ...getDefaultWorkspaceState(), + audioUrl: '', + codeDeltasToApply: null, + currentPlayerTime: 0, + description: null, + inputToApply: null, + playbackData: { + init: { + editorValue: '', + chapter: Chapter.SOURCE_1, + externalLibrary: ExternalLibraryName.NONE + }, + inputs: [] + }, + playbackDuration: 0, + playbackStatus: PlaybackStatus.paused, + sourcecastIndex: null, + title: null, + uid: null +} + +export const { actions: sourcecastActions, reducer: sourcecastReducer } = createWorkspaceSlice('sourcecast', defaultSourcecast, { + saveSourcecastData: { + prepare: ( + title: string, + description: string, + uid: string, + audio: Blob, + playbackData: PlaybackData, + ) => ({ + payload: { + title, + description, + uid, + audio, + audioUrl: window.URL.createObjectURL(audio), + playbackData, + } + }), + reducer(state, { payload }: PayloadAction<{ + title: string, + description: string, + uid: string, + audio: Blob, + audioUrl: string, // window.URL.createObjectURL(audio), + playbackData: PlaybackData, + }>) { + state.audioUrl = payload.audioUrl + state.description = payload.description + state.title = payload.title + state.playbackData = payload.playbackData + } + }, + setCurrentPlayerTime(state, { payload }: PayloadAction) { + state.currentPlayerTime = payload + }, + setCodeDeltasToApply(state, { payload }: PayloadAction) { + state.codeDeltasToApply = payload + }, + setInputToApply(state, { payload }: PayloadAction) { + state.inputToApply = payload + }, + setSourcecastData: { + prepare: ( + title: string, + description: string, + uid: string, + audioUrl: string, + playbackData: PlaybackData, + ) => ({ payload: { + title, + description, + uid, + audioUrl, + playbackData + }}), + reducer(state, { payload }: PayloadAction<{ + title: string, + description: string, + uid: string, + audioUrl: string, + playbackData: PlaybackData, + }>) { + state.title = payload.title + state.description = payload.description + state.uid = payload.uid + state.audioUrl = payload.audioUrl + state.playbackData = payload.playbackData + } + }, + setSourcecastPlaybackDuration(state, { payload }: PayloadAction) { + state.playbackDuration = payload + }, + setSourcecastPlaybackStatus(state, { payload }: PayloadAction) { + state.playbackStatus = payload + }, + updateSourcecastIndex(state, { payload }: PayloadAction) { + state.sourcecastIndex = payload + } +}) diff --git a/src/commons/redux/Sourcereel.ts b/src/commons/redux/workspace/SourcereelRedux.ts similarity index 86% rename from src/commons/redux/Sourcereel.ts rename to src/commons/redux/workspace/SourcereelRedux.ts index 134ff1d5b7..af9584ac31 100644 --- a/src/commons/redux/Sourcereel.ts +++ b/src/commons/redux/workspace/SourcereelRedux.ts @@ -1,19 +1,18 @@ import { PayloadAction } from '@reduxjs/toolkit' - import { Chapter } from "js-slang/dist/types"; import { Input, PlaybackData, RecordingStatus } from "src/features/sourceRecorder/SourceRecorderTypes"; -import { ExternalLibraryName } from "../application/types/ExternalTypes"; -import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "./workspace/WorkspaceRedux"; +import { ExternalLibraryName } from "../../application/types/ExternalTypes"; +import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "./WorkspaceRedux"; -export type SourcereelState = { +export type SourcereelWorkspaceState = { readonly playbackData: PlaybackData; readonly recordingStatus: RecordingStatus; readonly timeElapsedBeforePause: number; readonly timeResumed: number; } & WorkspaceState -export const defaultSourcereel: SourcereelState = { +export const defaultSourcereel: SourcereelWorkspaceState = { ...getDefaultWorkspaceState(), playbackData: { init: { @@ -51,6 +50,7 @@ export const { actions: sourcereelActions, reducer: sourcereelReducer } = create state.timeResumed = 0 }, timerResume: { + prepare: (timeNow: number, timeBefore: number) => ({ payload: { timeNow, timeBefore }}), reducer(state, { payload: { timeNow, timeBefore } }: PayloadAction>) { state.recordingStatus = RecordingStatus.recording state.timeElapsedBeforePause = timeBefore >= 0 ? timeBefore : state.timeElapsedBeforePause diff --git a/src/commons/redux/StoriesRedux.ts b/src/commons/redux/workspace/StoriesRedux.ts similarity index 93% rename from src/commons/redux/StoriesRedux.ts rename to src/commons/redux/workspace/StoriesRedux.ts index 6eaad97ece..93cc121c8c 100644 --- a/src/commons/redux/StoriesRedux.ts +++ b/src/commons/redux/workspace/StoriesRedux.ts @@ -2,10 +2,10 @@ import { createSlice,PayloadAction } from "@reduxjs/toolkit"; import { Chapter, Variant } from "js-slang/dist/types"; import { StoryData, StoryListView } from "src/features/stories/StoriesTypes"; -import { StoriesRole } from "../application/ApplicationTypes"; -import Constants from "../utils/Constants"; -import { createContext } from "../utils/JsSlangHelper"; -import { getDefaultPlaygroundState,PlaygroundWorkspaceState } from "./workspace/playground/PlaygroundBase"; +import { StoriesRole } from "../../application/ApplicationTypes"; +import Constants from "../../utils/Constants"; +import { createContext } from "../../utils/JsSlangHelper"; +import { getDefaultPlaygroundState,PlaygroundWorkspaceState } from "./playground/PlaygroundBase"; export type StoriesAuthState = { readonly userId?: number; diff --git a/src/commons/redux/workspace/WorkspaceRedux.ts b/src/commons/redux/workspace/WorkspaceRedux.ts index 91ec30db5c..bf9602dbda 100644 --- a/src/commons/redux/workspace/WorkspaceRedux.ts +++ b/src/commons/redux/workspace/WorkspaceRedux.ts @@ -1,13 +1,17 @@ -import { ActionReducerMapBuilder, combineReducers, createAction, createSlice, SliceCaseReducers,ValidateSliceCaseReducers } from "@reduxjs/toolkit"; -import { Context } from "js-slang/dist/types"; +import { ActionReducerMapBuilder, combineReducers, createSlice, SliceCaseReducers,ValidateSliceCaseReducers } from "@reduxjs/toolkit"; +import { Chapter, Context, Variant } from "js-slang/dist/types"; import { InterpreterOutput } from "src/commons/application/ApplicationTypes"; +import { Position } from "src/commons/editor/EditorTypes"; import Constants from "src/commons/utils/Constants"; import { createContext } from "src/commons/utils/JsSlangHelper"; import { DebuggerContext, EditorTabState } from "src/commons/workspace/WorkspaceTypes"; -import { defaultRepl,replActions,replReducer,ReplState } from "../ReplRedux"; -import { defaultSideContent, sideContentActions, sideContentReducer, SideContentState } from "../SideContentRedux"; -import { EditorState, getDefaultEditorState, getEditorReducer } from "./EditorRedux"; +import { createActions } from "../utils"; +import { WorkspaceManagerState } from "./AllWorkspacesRedux"; +import { StoriesEnvState } from "./StoriesRedux"; +import { EditorState, getDefaultEditorState, getEditorReducer } from "./subReducers/EditorRedux"; +import { defaultRepl,replActions,replReducer,ReplState } from "./subReducers/ReplRedux"; +import { defaultSideContent, NonStoryWorkspaceLocation, sideContentActions, SideContentLocation, sideContentReducer, SideContentState, StoryWorkspaceLocation } from "./subReducers/SideContentRedux"; export type WorkspaceState = { readonly context: Context; @@ -19,6 +23,8 @@ export type WorkspaceState = { readonly globals: Array<[string, any]>; + readonly hasUnsavedChanges: boolean + readonly isDebugging: boolean; readonly isEditorAutorun: boolean; readonly isEditorReadonly: boolean; @@ -31,7 +37,6 @@ export type WorkspaceState = { readonly programPostpendValue: string; readonly repl: ReplState; readonly sideContent: SideContentState - // readonly sharedbConnected: boolean; } export const getDefaultWorkspaceState = (initialTabs: EditorTabState[] = []): WorkspaceState => ({ @@ -45,6 +50,7 @@ export const getDefaultWorkspaceState = (initialTabs: EditorTabState[] = []): Wo editorState: getDefaultEditorState(initialTabs), enableDebugging: true, execTime: 1000, + hasUnsavedChanges: false, isDebugging: false, isEditorAutorun: false, isEditorReadonly: false, @@ -58,54 +64,47 @@ export const getDefaultWorkspaceState = (initialTabs: EditorTabState[] = []): Wo sideContent: defaultSideContent, }) -export const workspaceActions = { - beginClearContext: createAction('workspace/beginClearContext'), - beginDebugPause: createAction('workspace/beginDebugPause'), - beginInterruptExecution: createAction('workspace/beginInterruptExecution'), - debugReset: createAction('workspace/debugReset'), - debugResume: createAction('workspace/debugResume'), - endClearContext: createAction('workspace/endClearContext'), - endDebugPause: createAction('workspace/endDebugPause'), - endInterruptExecution: createAction('workspace/endInterruptExecution'), - evalEditor: createAction('workspace/evalEditor'), - evalRepl: createAction('workspace/evalRepl'), - setFolderMode: createAction('workspace/setFolderMode', (value: boolean) => ({ payload: value })), -} as const - -// const workspaceReducers = { -// debugReset(state: Draft) { -// state.isDebugging = false; -// state.isRunning = false; -// }, -// debugResume(state: Draft) { -// state.isDebugging = false; -// state.isRunning = true; -// }, -// endClearContext(state: Draft) { -// // TODO Investigate -// }, -// endDebugPause(state: Draft) { -// state.isDebugging = true; -// state.isRunning = false; -// }, -// endInterruptExecution(state: Draft) { -// // same as debug reset -// state.isDebugging = false; -// state.isRunning = false; -// }, -// evalEditor(state: Draft) { -// state.isDebugging = false; -// state.isRunning = true; -// }, -// evalRepl(state: Draft) { -// state.isRunning = true; -// }, -// setFolderMode(state: Draft, { payload }: PayloadAction) { -// state.isFolderModeEnabled = payload; -// }, -// } as const - -// type BaseWorkspaceReducers = typeof workspaceReducers +export const isNonStoryWorkspaceLocation = (location: SideContentLocation): location is NonStoryWorkspaceLocation => !location.startsWith('stories') + +export function getWorkspaceSelector(location: StoryWorkspaceLocation): (state: WorkspaceManagerState) => StoriesEnvState +export function getWorkspaceSelector(location: T): (state: WorkspaceManagerState) => WorkspaceManagerState[T] +export function getWorkspaceSelector(location :T) { + if (isNonStoryWorkspaceLocation(location)) { + return (state: WorkspaceManagerState) => state[location] + } else { + const [, storyEnv] = location.split('.') + return (state: WorkspaceManagerState) => state.stories.envs[storyEnv] + } +} + +export const workspaceActions = createActions('workspace', { + beginClearContext: ( + chapter: Chapter, + variant: Variant, + globals: Array<[string, any]>, + symbols: string[] + ) => ({ chapter, variant, globals, symbols }), + beginDebugPause: 0, + beginInterruptExecution: 0, + chapterSelect: (chapter: Chapter, variant: Variant) => ({ chapter, variant }), + debugReset: 0, + debugResume: 0, + endClearContext: (chapter: Chapter, variant: Variant, globals: Array<[string, any]>, symbols: string[]) => ({ + chapter, variant, globals, symbols + }), + endDebugPause: 0, + endInterruptExecution: 0, + evalEditor: 0, + evalRepl: 0, + navDeclaration: (position: Position) => position, + promptAutocomplete: (row: number, column: number, callback: any) => ({ row, column, callback }), + resetWorkspace: (options: Partial) => options, + setFolderMode: (value: boolean) => value, + toggleEditorAutorun: 0, + toggleFolderMode: 0, + updateHasUnsavedChanges: (value: boolean) => value, + updateWorkspace: (options: Partial) => options +}) export const createWorkspaceSlice = < TState extends WorkspaceState, @@ -130,43 +129,68 @@ export const createWorkspaceSlice = < initialState, reducers, extraReducers: builder => { - builder.addCase(workspaceActions.debugReset,(state) => { + builder.addCase(workspaceActions.debugReset, (state) => { state.isDebugging = false; state.isRunning = false; }) - builder.addCase(workspaceActions.debugResume,(state) => { + builder.addCase(workspaceActions.debugResume, (state) => { state.isDebugging = false; state.isRunning = true; }) - builder.addCase(workspaceActions.endClearContext,(state) => { - // TODO Investigate + builder.addCase(workspaceActions.endClearContext, (state, { payload }) => { + state.context = createContext( + payload.chapter, + payload.symbols, + '', + payload.variant + ) + + state.globals = payload.globals }) - builder.addCase(workspaceActions.endDebugPause,(state) => { + builder.addCase(workspaceActions.endDebugPause, (state) => { state.isDebugging = true; state.isRunning = false; }) - builder.addCase(workspaceActions.endInterruptExecution,(state) => { + builder.addCase(workspaceActions.endInterruptExecution, (state) => { // same as debug reset state.isDebugging = false; state.isRunning = false; }) - builder.addCase(workspaceActions.evalEditor,(state) => { + builder.addCase(workspaceActions.evalEditor, (state) => { state.isDebugging = false; state.isRunning = true; }) - builder.addCase(workspaceActions.evalRepl,(state) => { + builder.addCase(workspaceActions.evalRepl, (state) => { state.isRunning = true; }) + + builder.addCase(workspaceActions.resetWorkspace, (_, { payload }) => ({ + ...initialState, + ...payload + })) - builder.addCase(workspaceActions.setFolderMode,(state, { payload }) => { + builder.addCase(workspaceActions.setFolderMode, (state, { payload }) => { state.isFolderModeEnabled = payload; }) + builder.addCase(workspaceActions.toggleEditorAutorun, state => { + state.isEditorAutorun = !state.isEditorAutorun + }) + + builder.addCase(workspaceActions.updateWorkspace, (state, { payload }) => ({ + ...state, + ...payload + })) + + builder.addCase(workspaceActions.updateHasUnsavedChanges, (state, { payload }) => { + state.hasUnsavedChanges = payload + }) + builder.addCase(replActions.evalInterpreterError, state => { state.isDebugging = false; state.isRunning = false; @@ -187,10 +211,4 @@ export const createWorkspaceSlice = < }) } }); - // return { reducer, actions: { - // ...editorActions, - // ...sideContentActions, - // ...replActions, - // ...actions, - // }} } diff --git a/src/commons/redux/workspace/assessment/AssessmentBase.ts b/src/commons/redux/workspace/assessment/AssessmentBase.ts new file mode 100644 index 0000000000..4d70090272 --- /dev/null +++ b/src/commons/redux/workspace/assessment/AssessmentBase.ts @@ -0,0 +1,50 @@ +import { createAction,SliceCaseReducers, ValidateSliceCaseReducers } from "@reduxjs/toolkit"; +import { Value } from "js-slang/dist/types"; +import { AutogradingResult, Testcase } from "src/commons/assessment/AssessmentTypes"; +import { EditorTabState } from "src/commons/workspace/WorkspaceTypes"; + +import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "../WorkspaceRedux"; + +export type AssessmentState = WorkspaceState & { + readonly autogradingResults: AutogradingResult[] + readonly editorTestcases: Testcase[] +} + +export const assessmentActions = { + evalTestCaseFailure: createAction('testcases/evalTestCaseFailure', (value: Value, index: number) => ({ payload: { value, index } })), + evalTestCaseSuccess: createAction('testcases/evalTestCaseSuccess', (value: Value, index: number) => ({ payload: { value, index } })), + resetTestcase: createAction('testcases/resetTestcase', (index: number) => ({ payload: index })) +} as const + +export const getDefaultAssessmentState = (initialTabs: EditorTabState[] = []): AssessmentState => ({ + ...getDefaultWorkspaceState(initialTabs), + autogradingResults: [], + editorTestcases: [] +}) + +export const createAssessmentSlice = < + TState extends AssessmentState, + TReducers extends SliceCaseReducers, + TName extends string = string +>( + name: TName, + initialState: TState, + reducers: ValidateSliceCaseReducers +) => createWorkspaceSlice( + name, initialState, reducers, builder => { + builder.addCase(assessmentActions.evalTestCaseFailure, (state, { payload }) => { + state.editorTestcases[payload.index].errors = payload.value + state.editorTestcases[payload.index].result = undefined + }) + + builder.addCase(assessmentActions.evalTestCaseSuccess, (state, { payload }) => { + state.editorTestcases[payload.index].result = payload.value + state.editorTestcases[payload.index].errors = undefined + }) + + builder.addCase(assessmentActions.resetTestcase, (state, { payload }) => { + state.editorTestcases[payload].result = undefined + state.editorTestcases[payload].errors = undefined + }) + } +) diff --git a/src/commons/redux/workspace/assessment/AssessmentRedux.ts b/src/commons/redux/workspace/assessment/AssessmentRedux.ts new file mode 100644 index 0000000000..e314622aa2 --- /dev/null +++ b/src/commons/redux/workspace/assessment/AssessmentRedux.ts @@ -0,0 +1,25 @@ +import { PayloadAction } from "@reduxjs/toolkit"; + +import { AssessmentState, createAssessmentSlice, getDefaultAssessmentState } from "./AssessmentBase"; + +export type AssessmentWorkspaceState = AssessmentState & { + readonly currentAssessment?: number; + readonly currentQuestion?: number; +} + +export const defaultAssessment: AssessmentWorkspaceState = { + // TODO add default tab + ...getDefaultAssessmentState(), + currentAssessment: undefined, + currentQuestion: undefined, +} + +export const { actions: assessmentActions, reducer: assessmentReducer } = createAssessmentSlice('assessment', defaultAssessment, { + updateCurrentAssessmentId: { + prepare: (assessmentId: number, questionId: number) => ({ payload: { assessmentId, questionId }}), + reducer(state, { payload }: PayloadAction>) { + state.currentAssessment = payload.assessmentId + state.currentQuestion = payload.questionId + } + } +}) diff --git a/src/commons/redux/workspace/assessment/GithubAssesmentRedux.ts b/src/commons/redux/workspace/assessment/GithubAssesmentRedux.ts new file mode 100644 index 0000000000..f68cc1141f --- /dev/null +++ b/src/commons/redux/workspace/assessment/GithubAssesmentRedux.ts @@ -0,0 +1,17 @@ +import { defaultEditorValue } from "src/commons/application/ApplicationTypes"; + +import { AssessmentState, createAssessmentSlice, getDefaultAssessmentState } from "./AssessmentBase"; + +export type GitHubAssessmentWorkspaceState = AssessmentState + +export const defaultGithubAssessment: GitHubAssessmentWorkspaceState = { + ...getDefaultAssessmentState([{ + breakpoints: [], + filePath: undefined, + highlightedLines: [], + value: defaultEditorValue, + }]), + editorTestcases: [] +} + +export const { reducer: githubAssessmentReducer } = createAssessmentSlice('githubAssessment', defaultGithubAssessment, {}) diff --git a/src/commons/redux/workspace/GradingRedux.ts b/src/commons/redux/workspace/assessment/GradingRedux.ts similarity index 79% rename from src/commons/redux/workspace/GradingRedux.ts rename to src/commons/redux/workspace/assessment/GradingRedux.ts index 9444baadec..9b7c9f49fa 100644 --- a/src/commons/redux/workspace/GradingRedux.ts +++ b/src/commons/redux/workspace/assessment/GradingRedux.ts @@ -1,27 +1,29 @@ import { PayloadAction } from '@reduxjs/toolkit' import { SubmissionsTableFilters } from "src/commons/workspace/WorkspaceTypes"; -import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "./WorkspaceRedux"; +import { AssessmentState, createAssessmentSlice, getDefaultAssessmentState } from './AssessmentBase'; -export type GradingWorkspaceState = WorkspaceState & { - readonly submissionsTableFilters: SubmissionsTableFilters; +export type GradingWorkspaceState = AssessmentState & { readonly currentSubmission?: number; readonly currentQuestion?: number; readonly hasUnsavedChanges: boolean; + readonly submissionsTableFilters: SubmissionsTableFilters; } export const defaultGradingState: GradingWorkspaceState = ({ - ...getDefaultWorkspaceState(), + ...getDefaultAssessmentState(), + autogradingResults: [], submissionsTableFilters: { columnFilters: [], globalFilter: null }, currentSubmission: undefined, currentQuestion: undefined, + editorTestcases: [], hasUnsavedChanges: false }) -export const { actions: gradingActions, reducer: gradingReducer } = createWorkspaceSlice('grading', defaultGradingState, { +export const { actions: gradingActions, reducer: gradingReducer } = createAssessmentSlice('grading', defaultGradingState, { updateCurrentSubmissionId: { prepare: (currentSubmission: number, currentQuestion: number) => ({ payload: { currentQuestion, currentSubmission }}), reducer(state, { payload }: PayloadAction<{ currentSubmission: number, currentQuestion: number }>) { diff --git a/src/commons/redux/workspace/playground/PlaygroundBase.ts b/src/commons/redux/workspace/playground/PlaygroundBase.ts index 62f4a022dd..6dba738550 100644 --- a/src/commons/redux/workspace/playground/PlaygroundBase.ts +++ b/src/commons/redux/workspace/playground/PlaygroundBase.ts @@ -1,7 +1,8 @@ -import { ActionReducerMapBuilder, createAction, createReducer, SliceCaseReducers, ValidateSliceCaseReducers } from "@reduxjs/toolkit" +import { ActionReducerMapBuilder, createReducer, SliceCaseReducers, ValidateSliceCaseReducers } from "@reduxjs/toolkit" import { EditorTabState } from "src/commons/workspace/WorkspaceTypes" -import { createWorkspaceSlice, getDefaultWorkspaceState,WorkspaceState } from "../WorkspaceRedux" +import { createActions } from "../../utils" +import { createWorkspaceSlice, getDefaultWorkspaceState, WorkspaceState } from "../WorkspaceRedux" type PlaygroundAttr = { readonly breakpointSteps: number[] @@ -12,6 +13,7 @@ type PlaygroundAttr = { readonly usingEnv: boolean readonly usingSubst: boolean + readonly sharedbConnected: boolean } export type PlaygroundWorkspaceState = PlaygroundAttr & WorkspaceState @@ -25,41 +27,21 @@ export const getDefaultPlaygroundState = (initialTabs: EditorTabState[] = []): P updateEnv: true, usingEnv: false, usingSubst: false, + sharedbConnected: false }) -const playgroundBaseActions = { - changeStepLimit: createAction('playgroundBase/changeStepLimit', (payload: number) => ({ payload })), - toggleUpdateEnv: createAction('playgroundBase/toggleUpdateEnv', (payload: boolean) => ({ payload })), - toggleUsingEnv: createAction('playgroundBase/toggleUsingEnv', (payload: boolean) => ({ payload })), - toggleUsingSubst: createAction('playgroundBase/toggleUsingSubst', (payload: boolean) => ({ payload })), - updateBreakpointSteps: createAction('playgroundBase/updateBreakpointSteps', (payload: number[]) => ({ payload })), - updateEnvSteps: createAction('playgroundBase/updateEnvSteps', (payload: number) => ({ payload })), - updateEnvStepsTotal: createAction('playgroundBase/updateEnvStepsTotal', (payload: number) => ({ payload })) -} as const +export type PlaygroundWorkspaces = 'playground' | 'sicp' | `stories.${string}` -// const basePlaygroundReducers = { -// changeStepLimit(state: Draft, { payload }: PayloadAction) { -// state.stepLimit = payload -// }, -// toggleUpdateEnv(state: Draft, { payload }: PayloadAction) { -// state.updateEnv = payload -// }, -// toggleUsingEnv(state: Draft, { payload }: PayloadAction) { -// state.usingEnv = payload -// }, -// toggleUsingSubst(state: Draft, { payload }: PayloadAction) { -// state.usingSubst = payload -// }, -// updateBreakpointSteps(state: Draft, { payload }: PayloadAction) { -// state.breakpointSteps = payload -// }, -// updateEnvSteps(state: Draft, { payload }: PayloadAction) { -// state.envSteps = payload -// }, -// updateEnvStepsTotal(state: Draft, { payload }: PayloadAction) { -// state.envStepsTotal = payload -// } -// } as const +export const playgroundBaseActions = createActions('playgroundBase', { + changeStepLimit: (stepLimit: number) => stepLimit, + toggleUpdateEnv: (toggleUpdateEnv: boolean) => toggleUpdateEnv, + toggleUsingEnv: (toggleUsingEnv: boolean) => toggleUsingEnv, + toggleUsingSubst: (toggleUsingSubst: boolean) => toggleUsingSubst, + updateBreakpointSteps: (breakpointSteps: number[]) => breakpointSteps, + updateEnvSteps: (envSteps: number) => envSteps, + updateEnvStepsTotal: (envStepsTotal: number) => envStepsTotal, + updateSharedbConnected: (newValue: boolean) => newValue, +}) const reducerBuilder = (builder: ActionReducerMapBuilder) => { builder.addCase(playgroundBaseActions.changeStepLimit, (state, { payload }) => { @@ -88,6 +70,10 @@ const reducerBuilder = (builder: ActionReducerMapBuilder { state.envStepsTotal = payload }) + + builder.addCase(playgroundBaseActions.updateSharedbConnected, (state, { payload }) => { + state.sharedbConnected = payload + }) } export const basePlaygroundReducer = ( @@ -116,23 +102,3 @@ export const createPlaygroundSlice = < if (extraReducers) extraReducers(builder) } ) - -// export const createPlaygroundSlice = < -// TState extends PlaygroundWorkspaceState, -// TReducers extends SliceCaseReducers, -// TName extends string = string -// >( -// name: TName, -// initialState: TState, -// reducers: ValidateSliceCaseReducers, -// extraReducers?: (builder: ActionReducerMapBuilder) => void, -// ) => createWorkspaceSlice( -// name, -// initialState, -// reducers, -// builder => { - - -// if (extraReducers) extraReducers(builder) -// } -// ) diff --git a/src/commons/redux/workspace/playground/PlaygroundRedux.ts b/src/commons/redux/workspace/playground/PlaygroundRedux.ts index 2982f8d7b7..f213462f06 100644 --- a/src/commons/redux/workspace/playground/PlaygroundRedux.ts +++ b/src/commons/redux/workspace/playground/PlaygroundRedux.ts @@ -3,18 +3,18 @@ import { FSModule } from "browserfs/dist/node/core/FS"; import { Chapter, Variant } from "js-slang/dist/types"; import { compressToEncodedURIComponent } from "lz-string"; import * as qs from 'query-string'; -import { SagaIterator } from "redux-saga"; import { call, delay, put, race, select } from "redux-saga/effects"; import { defaultEditorValue, defaultLanguageConfig, getDefaultFilePath, OverallState, SALanguage } from "src/commons/application/ApplicationTypes"; import { ExternalLibraryName } from "src/commons/application/types/ExternalTypes"; import { retrieveFilesInWorkspaceAsRecord } from "src/commons/fileSystem/utils"; -import { safeTakeEvery as takeEvery } from "src/commons/sagas/SafeEffects"; import Constants from "src/commons/utils/Constants"; import { showSuccessMessage, showWarningMessage } from "src/commons/utils/notifications/NotificationsHelper"; import { EditorTabState } from "src/commons/workspace/WorkspaceTypes"; import { GitHubSaveInfo } from "src/features/github/GitHubTypes"; import { PersistenceFile } from "src/features/persistence/PersistenceTypes"; +import { combineSagaHandlers } from "../../utils"; +import { EditorState } from "../subReducers/EditorRedux"; import { createPlaygroundSlice, getDefaultPlaygroundState, PlaygroundWorkspaceState } from "./PlaygroundBase"; export type PlaygroundState = PlaygroundWorkspaceState & { @@ -33,7 +33,8 @@ export const defaultPlayground: PlaygroundState = { value: defaultEditorValue }]), githubSaveInfo: { repoName: '', filePath: '' }, - languageConfig: defaultLanguageConfig + languageConfig: defaultLanguageConfig, + sharedbConnected: false } const { actions: playgroundWorkspaceActions, reducer } = createPlaygroundSlice('playground', defaultPlayground, { @@ -56,16 +57,19 @@ const { actions: playgroundWorkspaceActions, reducer } = createPlaygroundSlice(' export { reducer as playgroundReducer } -export const playgroundActions = { - ...playgroundWorkspaceActions, +const playgroundSagaActions = { generateLzString: createAction('playground/generateLzString'), shortenUrl: createAction('playground/shortenURL', (url: string) => ({ payload: url })) } -export function* playgroundSaga(): SagaIterator { - yield takeEvery(playgroundActions.generateLzString, updateQueryString) +export const playgroundActions = { + ...playgroundWorkspaceActions, + ...playgroundSagaActions +} - yield takeEvery(playgroundActions.shortenUrl, function* ({ payload: keyword }): SagaIterator { +export const PlaygroundSaga = combineSagaHandlers(playgroundSagaActions, { + generateLzString: updateQueryString, + shortenUrl: function* ({ payload: keyword }) { const queryString = yield select((state: OverallState) => state.playground.queryString); const errorMsg = 'ERROR'; @@ -96,8 +100,8 @@ export function* playgroundSaga(): SagaIterator { yield call(showSuccessMessage, resp.message); } yield put(playgroundWorkspaceActions.updateShortURL(Constants.urlShortenerBase + resp.url.keyword)); - }) -} + } +}) function* updateQueryString() { const isFolderModeEnabled: boolean = yield select( @@ -111,24 +115,24 @@ function* updateQueryString() { 'playground', fileSystem ); - const editorTabs: EditorTabState[] = yield select( - (state: OverallState) => state.workspaces.playground.editorTabs - ); + + const { editorTabs, activeEditorTabIndex }: EditorState = yield select( + (state: OverallState) => state.workspaces.playground.editorState + ) + const editorTabFilePaths = editorTabs .map((editorTab: EditorTabState) => editorTab.filePath) .filter((filePath): filePath is string => filePath !== undefined); - const activeEditorTabIndex: number | null = yield select( - (state: OverallState) => state.workspaces.playground.activeEditorTabIndex - ); + const chapter: Chapter = yield select( (state: OverallState) => state.workspaces.playground.context.chapter ); const variant: Variant = yield select( (state: OverallState) => state.workspaces.playground.context.variant ); - const external: ExternalLibraryName = yield select( - (state: OverallState) => state.workspaces.playground.externalLibrary - ); + // const external: ExternalLibraryName = yield select( + // (state: OverallState) => state.workspaces.playground.externalLibrary + // ); const execTime: number = yield select( (state: OverallState) => state.workspaces.playground.execTime ); @@ -139,7 +143,7 @@ function* updateQueryString() { tabIdx: activeEditorTabIndex, chap: chapter, variant, - ext: external, + ext: ExternalLibraryName.NONE, exec: execTime }); yield put(playgroundWorkspaceActions.changeQueryString(newQueryString)); diff --git a/src/commons/redux/workspace/EditorRedux.ts b/src/commons/redux/workspace/subReducers/EditorRedux.ts similarity index 83% rename from src/commons/redux/workspace/EditorRedux.ts rename to src/commons/redux/workspace/subReducers/EditorRedux.ts index 86c7b23976..c244796533 100644 --- a/src/commons/redux/workspace/EditorRedux.ts +++ b/src/commons/redux/workspace/subReducers/EditorRedux.ts @@ -1,7 +1,9 @@ -import { createAction, createReducer } from "@reduxjs/toolkit" +import { createReducer } from "@reduxjs/toolkit" import { HighlightedLines, Position } from "src/commons/editor/EditorTypes" import { EditorTabState } from "src/commons/workspace/WorkspaceTypes" +import { createActions } from "../../utils" + export type EditorState = { readonly activeEditorTabIndex: number | null readonly editorSessionId: string @@ -19,23 +21,27 @@ export const getDefaultEditorState = (defaultTabs: EditorTabState[] = []): Edito isEditorReadonly: false }) -export const editorActions = { - addEditorTab: createAction('editorBase/addEditorTab', (filePath: string, editorValue: string) => ({ payload: { filePath, editorValue }})), - moveCursor: createAction('editorBase/moveCursor', (editorTabIndex: number, newCursorPosition: Position) => ({ payload: { editorTabIndex, newCursorPosition }})), - removeEditorTab: createAction('editorBase/removeEditorTab', (editorTabIndex: number) => ({ payload: editorTabIndex })), - setEditorSessionId: createAction('editorBase/setEditorSessionId', (payload: string) => ({ payload })), - setIsEditorAutorun: createAction('editorBase/setIsEditorAutorun', (payload: boolean) => ({ payload })), - setIsEditorReadonly: createAction('editorBase/setIsEditorReadonly', (payload: boolean) => ({ payload })), - shiftEditorTab: createAction('editorAction/shiftEditorTab', (previousIndex: number, newIndex: number) => ({ payload: { previousIndex, newIndex }})), - updateActiveEditorTab: createAction('editorBase/updateActiveEditorTab', (payload: Partial | undefined) => ({ payload })), - updateActiveEditorTabIndex: createAction('editorBase/updateActiveEditorTabIndex', (payload: number | null) => ({ payload })), - updateEditorBreakpoints: createAction('editorBase/updateEditorBreakpoints', (editorTabIndex: number, newBreakpoints: string[]) => ({ payload: { editorTabIndex, newBreakpoints }})), - updateEditorHighlightedLines: createAction('editorBase/updateEditorHighlightedLines', (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }})), - updateEditorHighlightedLinesAgenda: createAction('editorBase/updateEditorHighlightedLinesAgenda', (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ payload: { editorTabIndex, newHighlightedLines }})), - updateEditorValue: createAction('editorBase/updateEditorValue', (editorTabIndex: number, newEditorValue: string) => ({ payload: { editorTabIndex, newEditorValue }})), -} as const - -export const getEditorReducer = (defaultTabs: EditorTabState[] = [] ) => createReducer( +export const editorActions = createActions('editorBase', { + addEditorTab: (filePath: string, editorValue: string) => ({ filePath, editorValue }), + moveCursor: (editorTabIndex: number, newCursorPosition: Position) => ({ editorTabIndex, newCursorPosition }), + removeEditorTab: (editorTabIndex: number) => editorTabIndex, + removeEditorTabForFile: (removedFilePath: string) => removedFilePath, + removeEditorTabsForDirectory: (removedDirectoryPath: string) => removedDirectoryPath, + renameEditorTabForFile: (oldPath: string, newPath: string) => ({ oldPath, newPath }), + renameEditorTabsForDirectory: (oldPath: string, newPath: string) => ({ oldPath, newPath }), + setEditorSessionId: (editorSessionId: string) => editorSessionId, + setIsEditorAutorun: (isEditorAutorun: boolean) => isEditorAutorun, + setIsEditorReadonly: (isEditorReadonly: boolean) => isEditorReadonly, + shiftEditorTab: (previousIndex: number, newIndex: number) => ({ previousIndex, newIndex }), + updateActiveEditorTab: (editorOptions: Partial | undefined) => editorOptions, + updateActiveEditorTabIndex: (activeEditorTabIndex: number | null) => activeEditorTabIndex, + updateEditorBreakpoints: (editorTabIndex: number, newBreakpoints: string[]) => ({ editorTabIndex, newBreakpoints }), + updateEditorHighlightedLines: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ editorTabIndex, newHighlightedLines }), + updateEditorHighlightedLinesAgenda: (editorTabIndex: number, newHighlightedLines: HighlightedLines[]) => ({ editorTabIndex, newHighlightedLines }), + updateEditorValue: (editorTabIndex: number, newEditorValue: string) => ({ editorTabIndex, newEditorValue }), +}) + +export const getEditorReducer = (defaultTabs: EditorTabState[] = []) => createReducer( getDefaultEditorState(defaultTabs), builder => { builder.addCase(editorActions.addEditorTab, (state, { payload }) => { @@ -94,6 +100,82 @@ export const getEditorReducer = (defaultTabs: EditorTabState[] = [] ) => createR state.editorTabs.splice(editorTabIndex, 1) }) + builder.addCase(editorActions.removeEditorTabForFile, (state, { payload: removedFilePath }) => { + const editorTabs = state.editorTabs; + const editorTabIndexToRemove = editorTabs.findIndex( + (editorTab: EditorTabState) => editorTab.filePath === removedFilePath + ); + if (editorTabIndexToRemove === -1) return + + const newEditorTabs = editorTabs.filter( + (editorTab: EditorTabState, index: number) => index !== editorTabIndexToRemove + ); + + const activeEditorTabIndex = state.activeEditorTabIndex; + state.activeEditorTabIndex = getNextActiveEditorTabIndexAfterTabRemoval( + activeEditorTabIndex, + editorTabIndexToRemove, + newEditorTabs.length + ); + state.editorTabs = newEditorTabs + }) + + builder.addCase(editorActions.removeEditorTabsForDirectory, (state, { payload: removedDirectoryPath }) => { + const editorTabs = state.editorTabs; + const editorTabIndicesToRemove = editorTabs + .map((editorTab: EditorTabState, index: number) => { + if (editorTab.filePath?.startsWith(removedDirectoryPath)) { + return index; + } + return null; + }) + .filter((index: number | null): index is number => index !== null); + if (editorTabIndicesToRemove.length === 0) return + + let newActiveEditorTabIndex = state.activeEditorTabIndex; + const newEditorTabs = [...editorTabs]; + for (let i = editorTabIndicesToRemove.length - 1; i >= 0; i--) { + const editorTabIndexToRemove = editorTabIndicesToRemove[i]; + newEditorTabs.splice(editorTabIndexToRemove, 1); + newActiveEditorTabIndex = getNextActiveEditorTabIndexAfterTabRemoval( + newActiveEditorTabIndex, + editorTabIndexToRemove, + newEditorTabs.length + ); + } + + state.activeEditorTabIndex = newActiveEditorTabIndex + state.editorTabs = newEditorTabs + }) + + builder.addCase(editorActions.renameEditorTabForFile, (state, { payload: { oldPath, newPath } }) => { + const editorTabs = state.editorTabs; + const newEditorTabs = editorTabs.map((editorTab: EditorTabState) => + editorTab.filePath === oldPath + ? { + ...editorTab, + filePath: newPath + } + : editorTab + ); + + state.editorTabs = newEditorTabs + }) + + builder.addCase(editorActions.renameEditorTabsForDirectory, (state, { payload: { oldPath, newPath }}) => { + const editorTabs = state.editorTabs; + const newEditorTabs = editorTabs.map((editorTab: EditorTabState) => + editorTab.filePath?.startsWith(oldPath) + ? { + ...editorTab, + filePath: editorTab.filePath?.replace(oldPath, newPath) + } + : editorTab + ); + + state.editorTabs = newEditorTabs + }) + builder.addCase(editorActions.setEditorSessionId, (state, { payload }) => { state.editorSessionId = payload }) diff --git a/src/commons/redux/ReplRedux.ts b/src/commons/redux/workspace/subReducers/ReplRedux.ts similarity index 95% rename from src/commons/redux/ReplRedux.ts rename to src/commons/redux/workspace/subReducers/ReplRedux.ts index 048b39a5f7..336b2e9b3e 100644 --- a/src/commons/redux/ReplRedux.ts +++ b/src/commons/redux/workspace/subReducers/ReplRedux.ts @@ -2,8 +2,8 @@ import { createAction, createReducer } from "@reduxjs/toolkit"; import { SourceError, Value } from "js-slang/dist/types"; import { stringify } from "js-slang/dist/utils/stringify"; -import { CodeOutput, InterpreterOutput } from "../application/ApplicationTypes" -import Constants from "../utils/Constants"; +import { CodeOutput, InterpreterOutput } from "../../../application/ApplicationTypes" +import Constants from "../../../utils/Constants"; export type ReplState = { readonly output: InterpreterOutput[] @@ -34,7 +34,10 @@ export const replActions = { evalInterpreterError: createAction('repl/evalInterpreterError', (payload: SourceError[]) => ({ payload })), evalInterpreterSuccess: createAction('repl/evalInterpreterSuccess', (payload: Value) => ({ payload })), handleConsoleLog: createAction('repl/handleConsoleLog', (payload: string[]) => ({ payload })), - sendReplInputToOutput: createAction('repl/sendReplInputToOutput', (payload: CodeOutput) => ({ payload })), + sendReplInputToOutput: createAction('repl/sendReplInputToOutput', (output: string): { payload: CodeOutput } => ({ payload: { + type: 'code', + value: output + }})), updateReplValue: createAction('repl/updateReplValue', (payload: string) => ({ payload })), } as const diff --git a/src/commons/redux/SideContentRedux.ts b/src/commons/redux/workspace/subReducers/SideContentRedux.ts similarity index 70% rename from src/commons/redux/SideContentRedux.ts rename to src/commons/redux/workspace/subReducers/SideContentRedux.ts index 97694d5a34..6e88f26483 100644 --- a/src/commons/redux/SideContentRedux.ts +++ b/src/commons/redux/workspace/subReducers/SideContentRedux.ts @@ -1,10 +1,13 @@ import { createAction, createReducer } from '@reduxjs/toolkit' +import { Context } from 'js-slang' -import { getDynamicTabs, getTabId } from '../sideContent/SideContentHelper' -import { SideContentTab, SideContentType } from '../sideContent/SideContentTypes' -import { DebuggerContext, WorkspaceLocation } from '../workspace/WorkspaceTypes' +import { getDynamicTabs, getTabId } from '../../../sideContent/SideContentHelper' +import { SideContentTab, SideContentType } from '../../../sideContent/SideContentTypes' +import { DebuggerContext, WorkspaceLocation } from '../../../workspace/WorkspaceTypes' -export type SideContentLocation = Exclude | `stories.${string}` +export type NonStoryWorkspaceLocation = Exclude +export type StoryWorkspaceLocation = `stories.${string}` +export type SideContentLocation = NonStoryWorkspaceLocation | StoryWorkspaceLocation export type SideContentState = { dynamicTabs: SideContentTab[] @@ -22,7 +25,17 @@ export const sideContentActions = { beginAlertSideContent: createAction('sideContent/beginAlertSideContent', (newId: SideContentType) => ({ payload: newId })), changeSideContentHeight: createAction('sideContent/changeSideContentHeight', (payload: number) => ({ payload })), endAlertSideContentHeight: createAction('sideContent/endAlertSideContentHeight', (payload: SideContentType) => ({ payload })), - notifyProgramEvaluated: createAction('sideContent/notifyProgramEvaluated', (payload: DebuggerContext) => ({ payload })), + notifyProgramEvaluated: createAction('sideContent/notifyProgramEvaluated', ( + result: any, + lastDebuggerResult: any, + code: string, + context: Context, + ): { payload: DebuggerContext } => ({ payload: { + result, + lastDebuggerResult, + code, + context + } })), visitSideContent: createAction('sideContent/visitSideContent', (payload: SideContentType) => ({ payload })), } diff --git a/src/commons/repl/Repl.tsx b/src/commons/repl/Repl.tsx index acf1588770..3d3eb13a7a 100644 --- a/src/commons/repl/Repl.tsx +++ b/src/commons/repl/Repl.tsx @@ -7,7 +7,6 @@ import * as React from 'react'; import { HotKeys } from 'react-hotkeys'; import { InterpreterOutput } from '../application/ApplicationTypes'; -import { ExternalLibraryName } from '../application/types/ExternalTypes'; import { ReplInput } from './ReplInput'; import { OutputProps } from './ReplTypes'; @@ -21,7 +20,7 @@ type StateProps = { usingSubst?: boolean; sourceChapter: Chapter; sourceVariant: Variant; - externalLibrary: ExternalLibraryName; + // externalLibrary: ExternalLibraryName; disableScrolling?: boolean; }; diff --git a/src/commons/repl/ReplInput.tsx b/src/commons/repl/ReplInput.tsx index 28db848edc..3a3365165a 100644 --- a/src/commons/repl/ReplInput.tsx +++ b/src/commons/repl/ReplInput.tsx @@ -26,7 +26,7 @@ type StateProps = { replValue: string; sourceChapter: Chapter; sourceVariant: Variant; - externalLibrary: ExternalLibraryName; + // externalLibrary: ExternalLibraryName; disableScrolling?: boolean; }; @@ -86,7 +86,7 @@ export const ReplInput: React.FC = (props: ReplInputProps) => { }); // see the comment above this same call in Editor.tsx - selectMode(props.sourceChapter, props.sourceVariant, props.externalLibrary); + selectMode(props.sourceChapter, props.sourceVariant, ExternalLibraryName.NONE); const replButtons = () => props.replButtons; @@ -94,7 +94,7 @@ export const ReplInput: React.FC = (props: ReplInputProps) => { <> Date: Fri, 22 Sep 2023 14:13:33 +0800 Subject: [PATCH 07/13] Update workspaces to use hooks --- .../AssessmentWorkspace.tsx | 20 ++++---- .../editingWorkspace/EditingWorkspace.tsx | 14 ++++-- .../subcomponents/GradingWorkspace.tsx | 16 +++---- src/pages/academy/sourcereel/Sourcereel.tsx | 26 +++++++---- .../GitHubAssessmentWorkspace.tsx | 17 ++++--- src/pages/playground/Playground.tsx | 46 +++++++++++-------- src/pages/sourcecast/Sourcecast.tsx | 28 ++++++----- 7 files changed, 98 insertions(+), 69 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index a6522697d2..50c474b28d 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -58,6 +58,7 @@ import { Position } from '../editor/EditorTypes'; import Markdown from '../Markdown'; import { MobileSideContentProps } from '../mobileWorkspace/mobileSideContent/MobileSideContent'; import MobileWorkspace, { MobileWorkspaceProps } from '../mobileWorkspace/MobileWorkspace'; +import { useEditorState, useRepl, useSideContent, useWorkspace } from '../redux/workspace/Hooks'; import SideContentAutograder from '../sideContent/content/SideContentAutograder'; import SideContentContestLeaderboard from '../sideContent/content/SideContentContestLeaderboard'; import SideContentContestVotingContainer from '../sideContent/content/SideContentContestVotingContainer'; @@ -110,29 +111,28 @@ const AssessmentWorkspace: React.FC = props => { const { isMobileBreakpoint } = useResponsive(); const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); - const [selectedTab, setSelectedTab] = useState( + const { selectedTab, setSelectedTab, height: sideContentHeight } = useSideContent(workspaceLocation, assessment?.questions[props.questionId].grader !== undefined - ? SideContentType.grading - : SideContentType.questionOverview - ); + ? SideContentType.grading + : SideContentType.questionOverview + ) + + const { activeEditorTabIndex, editorTabs } = useEditorState(workspaceLocation) + const { replValue } = useRepl(workspaceLocation) const navigate = useNavigate(); const { courseId } = useTypedSelector(state => state.session); const { isFolderModeEnabled, - activeEditorTabIndex, - editorTabs, autogradingResults, editorTestcases, hasUnsavedChanges, isRunning, output, - replValue, - sideContentHeight, currentAssessment: storedAssessmentId, currentQuestion: storedQuestionId - } = useTypedSelector(store => store.workspaces[workspaceLocation]); + } = useWorkspace(workspaceLocation) const dispatch = useDispatch(); const { @@ -233,7 +233,7 @@ const AssessmentWorkspace: React.FC = props => { if (!isMobileBreakpoint && mobileOnlyTabIds.includes(selectedTab)) { setSelectedTab(SideContentType.questionOverview); } - }, [isMobileBreakpoint, props, selectedTab]); + }, [isMobileBreakpoint, props, selectedTab, setSelectedTab]); /* ================== onChange handlers diff --git a/src/commons/editingWorkspace/EditingWorkspace.tsx b/src/commons/editingWorkspace/EditingWorkspace.tsx index bef8fd0c8b..a98f4812e7 100644 --- a/src/commons/editingWorkspace/EditingWorkspace.tsx +++ b/src/commons/editingWorkspace/EditingWorkspace.tsx @@ -46,6 +46,7 @@ import { TextAreaContent } from '../editingWorkspaceSideContent/EditingWorkspace import { convertEditorTabStateToProps } from '../editor/EditorContainer'; import { Position } from '../editor/EditorTypes'; import Markdown from '../Markdown'; +import { useEditorState, useRepl, useSideContent } from '../redux/workspace/Hooks'; import SideContentToneMatrix from '../sideContent/content/SideContentToneMatrix'; import { SideContentProps } from '../sideContent/SideContent'; import { SideContentTab, SideContentType } from '../sideContent/SideContentTypes'; @@ -98,14 +99,17 @@ const EditingWorkspace: React.FC = props => { const [originalMaxXp, setOriginalMaxXp] = useState(0); const navigate = useNavigate(); + const { activeEditorTabIndex, editorTabs } = useEditorState(workspaceLocation) + const { replValue } = useRepl(workspaceLocation) + const { height: sideContentHeight } = useSideContent( + workspaceLocation, + SideContentType.introduction + ) + const { isFolderModeEnabled, - activeEditorTabIndex, - editorTabs, isRunning, output, - replValue, - sideContentHeight, currentAssessment: storedAssessmentId, currentQuestion: storedQuestionId } = useTypedSelector(store => store.workspaces[workspaceLocation]); @@ -710,7 +714,7 @@ const EditingWorkspace: React.FC = props => { replValue: replValue, sourceChapter: question?.library?.chapter || Chapter.SOURCE_4, sourceVariant: Variant.DEFAULT, - externalLibrary: question?.library?.external?.name || 'NONE', + // externalLibrary: question?.library?.external?.name || 'NONE', replButtons: replButtons() } }; diff --git a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx index 89c1675ab5..57772421bb 100644 --- a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx +++ b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx @@ -2,10 +2,11 @@ import { Classes, NonIdealState, Spinner, SpinnerSize } from '@blueprintjs/core' import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router'; import { fetchGrading } from 'src/commons/application/actions/SessionActions'; +import { useEditorState, useRepl, useSideContent, useWorkspace } from 'src/commons/redux/workspace/Hooks'; import SideContentToneMatrix from 'src/commons/sideContent/content/SideContentToneMatrix'; import { showSimpleErrorDialog } from 'src/commons/utils/DialogHelper'; import { useTypedSelector } from 'src/commons/utils/Hooks'; @@ -74,23 +75,22 @@ const unansweredPrependValue: string = `// This answer does not have significant const GradingWorkspace: React.FC = props => { const navigate = useNavigate(); - const [selectedTab, setSelectedTab] = useState(SideContentType.grading); + const { selectedTab, setSelectedTab, height: sideContentHeight } = useSideContent(workspaceLocation, SideContentType.grading) + const { editorTabs, activeEditorTabIndex } = useEditorState(workspaceLocation) + const { replValue } = useRepl(workspaceLocation) + // const [selectedTab, setSelectedTab] = useState(SideContentType.grading); const grading = useTypedSelector(state => state.session.gradings.get(props.submissionId)); const courseId = useTypedSelector(state => state.session.courseId); const { autogradingResults, isFolderModeEnabled, - activeEditorTabIndex, - editorTabs, editorTestcases, isRunning, output, - replValue, - sideContentHeight, currentSubmission: storedSubmissionId, currentQuestion: storedQuestionId - } = useTypedSelector(state => state.workspaces[workspaceLocation]); + } = useWorkspace(workspaceLocation) const dispatch = useDispatch(); const { @@ -495,7 +495,7 @@ const GradingWorkspace: React.FC = props => { replValue: replValue, sourceChapter: question?.library?.chapter || Chapter.SOURCE_4, sourceVariant: question?.library?.variant ?? Variant.DEFAULT, - externalLibrary: question?.library?.external?.name || 'NONE', + // externalLibrary: question?.library?.external?.name || 'NONE', replButtons: replButtons() } }; diff --git a/src/pages/academy/sourcereel/Sourcereel.tsx b/src/pages/academy/sourcereel/Sourcereel.tsx index e3cc5a408d..02d526bb3d 100644 --- a/src/pages/academy/sourcereel/Sourcereel.tsx +++ b/src/pages/academy/sourcereel/Sourcereel.tsx @@ -2,7 +2,7 @@ import { Classes, Pre } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { beginDebuggerPause, @@ -10,6 +10,7 @@ import { debuggerReset, debuggerResume } from 'src/commons/application/actions/InterpreterActions'; +import { useEditorState, useRepl, useSideContent } from 'src/commons/redux/workspace/Hooks'; import { fetchSourcecastIndex } from 'src/features/sourceRecorder/sourcecast/SourcecastActions'; import { saveSourcecastData, @@ -85,8 +86,6 @@ const workspaceLocation: WorkspaceLocation = 'sourcereel'; const sourcecastLocation: WorkspaceLocation = 'sourcecast'; const Sourcereel: React.FC = () => { - const [selectedTab, setSelectedTab] = useState(SideContentType.sourcereel); - const courseId = useTypedSelector(state => state.session.courseId); const { chapter: sourceChapter, variant: sourceVariant } = useTypedSelector( state => state.workspaces[workspaceLocation].context @@ -100,11 +99,18 @@ const Sourcereel: React.FC = () => { playbackStatus, sourcecastIndex } = useTypedSelector(state => state.workspaces.sourcecast); + + const { activeEditorTabIndex, editorTabs } = useEditorState(workspaceLocation) + const { selectedTab, setSelectedTab, height: sideContentHeight } = useSideContent( + workspaceLocation, + SideContentType.sourcereel + ) + + const { replValue } = useRepl(workspaceLocation) + const { isFolderModeEnabled, - activeEditorTabIndex, - editorTabs, - externalLibrary: externalLibraryName, + // externalLibrary: externalLibraryName, isDebugging, isEditorAutorun, isEditorReadonly, @@ -112,8 +118,6 @@ const Sourcereel: React.FC = () => { output, playbackData, recordingStatus, - replValue, - sideContentHeight, timeElapsedBeforePause, timeResumed } = useTypedSelector(store => store.workspaces[workspaceLocation]); @@ -178,7 +182,9 @@ const Sourcereel: React.FC = () => { const handleRecordInit = () => { const initData: PlaybackData['init'] = { chapter: sourceChapter, - externalLibrary: externalLibraryName, + // TODO investigate + externalLibrary: ExternalLibraryName.NONE, + // externalLibrary: externalLibraryName, // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. editorValue: editorTabs[0].value }; @@ -379,7 +385,7 @@ const Sourcereel: React.FC = () => { handleReplValueChange: workspaceHandlers.handleReplValueChange, sourceChapter: sourceChapter, sourceVariant: sourceVariant, - externalLibrary: externalLibraryName, + // externalLibrary: externalLibraryName, replButtons: [evalButton, clearButton] }, sideBarProps: { diff --git a/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx b/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx index 91682d69ff..5c8f5ca08a 100644 --- a/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx +++ b/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx @@ -15,7 +15,8 @@ import { isEqual } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useLocation, useNavigate } from 'react-router'; -import { useResponsive, useTypedSelector } from 'src/commons/utils/Hooks'; +import { useEditorState, useRepl, useSideContent, useWorkspace } from 'src/commons/redux/workspace/Hooks'; +import { useResponsive } from 'src/commons/utils/Hooks'; import { browseReplHistoryDown, browseReplHistoryUp, @@ -174,20 +175,22 @@ const GitHubAssessmentWorkspace: React.FC = () => { const assessmentOverview = location.state as GHAssessmentOverview; const [showBriefingOverlay, setShowBriefingOverlay] = useState(false); - const [selectedTab, setSelectedTab] = useState(SideContentType.questionOverview); const { isMobileBreakpoint } = useResponsive(); + const { activeEditorTabIndex, editorTabs } = useEditorState(workspaceLocation) + const { replValue } = useRepl(workspaceLocation) + const { selectedTab, setSelectedTab, height: sideContentHeight } = useSideContent( + workspaceLocation, + SideContentType.questionOverview + ) + const { isFolderModeEnabled, - activeEditorTabIndex, - editorTabs, editorTestcases, hasUnsavedChanges, isRunning, output, - replValue, - sideContentHeight - } = useTypedSelector(state => state.workspaces.githubAssessment); + } = useWorkspace('githubAssessment'); /** * Should be called to change the task number, rather than using setCurrentTaskNumber diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index fb84485c19..766455b268 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -8,7 +8,7 @@ import { isEqual } from 'lodash'; import { decompressFromEncodedURIComponent } from 'lz-string'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { HotKeys } from 'react-hotkeys'; -import { useDispatch, useStore } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { useLocation, useNavigate } from 'react-router'; import { AnyAction, Dispatch } from 'redux'; import { @@ -26,6 +26,7 @@ import { setEditorSessionId, setSharedbConnected } from 'src/commons/collabEditing/CollabEditingActions'; +import { useEditorState, useRepl, useSideContent, useWorkspace } from 'src/commons/redux/workspace/Hooks'; import { useResponsive, useTypedSelector } from 'src/commons/utils/Hooks'; import { showFullJSWarningOnUrlLoad, @@ -87,7 +88,6 @@ import { getDefaultFilePath, getLanguageConfig, isSourceLanguage, - OverallState, ResultOutput, SALanguage } from '../../commons/application/ApplicationTypes'; @@ -240,29 +240,34 @@ const Playground: React.FC = props => { const [deviceSecret, setDeviceSecret] = useState(); const location = useLocation(); const navigate = useNavigate(); - const store = useStore(); const searchParams = new URLSearchParams(location.search); const shouldAddDevice = searchParams.get('add_device'); - // Selectors and handlers migrated over from deprecated withRouter implementation const { - editorTabs, + activeEditorTabIndex, editorSessionId, + editorTabs, + } = useEditorState(workspaceLocation) + + const { + replValue + } = useRepl(workspaceLocation) + + // Selectors and handlers migrated over from deprecated withRouter implementation + const { execTime, stepLimit, isEditorAutorun, isRunning, isDebugging, output, - replValue, - sideContentHeight, sharedbConnected, usingSubst, usingEnv, isFolderModeEnabled, - activeEditorTabIndex, context: { chapter: playgroundSourceChapter, variant: playgroundSourceVariant } - } = useTypedSelector(state => state.workspaces[workspaceLocation]); + } = useWorkspace(workspaceLocation) + const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); const { queryString, shortURL, persistenceFile, githubSaveInfo } = useTypedSelector( state => state.playground @@ -311,9 +316,14 @@ const Playground: React.FC = props => { const [lastEdit, setLastEdit] = useState(new Date()); const [isGreen, setIsGreen] = useState(false); - const [selectedTab, setSelectedTab] = useState( + const { + selectedTab, + setSelectedTab, + height: sideContentHeight + } = useSideContent(workspaceLocation, shouldAddDevice ? SideContentType.remoteExecution : SideContentType.introduction - ); + ) + const [hasBreakpoints, setHasBreakpoints] = useState(false); const [sessionId, setSessionId] = useState(() => initSession('playground', { @@ -332,9 +342,9 @@ const Playground: React.FC = props => { useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; // this is still used by remote execution (EV3) // specifically, for the editor Ctrl+B to work - const externalLibraryName = useTypedSelector( - state => state.workspaces.playground.externalLibrary - ); + // const externalLibraryName = useTypedSelector( + // state => state.workspaces.playground.externalLibrary + // ); useEffect(() => { // When the editor session Id changes, then treat it as a new session. @@ -394,7 +404,7 @@ const Playground: React.FC = props => { } else if (!isMobileBreakpoint && mobileOnlyTabIds.includes(selectedTab)) { setSelectedTab(SideContentType.introduction); } - }, [isMobileBreakpoint, selectedTab]); + }, [isMobileBreakpoint, selectedTab, setSelectedTab]); const handlers = useMemo( () => ({ @@ -671,8 +681,8 @@ const Playground: React.FC = props => { const getEditorValue = useCallback( // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - () => store.getState().workspaces[workspaceLocation].editorTabs[0].value, - [store, workspaceLocation] + () => editorTabs[activeEditorTabIndex].value, + [editorTabs, activeEditorTabIndex] ); const sessionButtons = useMemo( @@ -930,7 +940,7 @@ const Playground: React.FC = props => { onSelectionChange: onSelectionChangeMethod, onLoad: isSicpEditor && props.prependLength ? onLoadMethod : undefined, sourceChapter: languageConfig.chapter, - externalLibraryName, + // externalLibraryName, sourceVariant: languageConfig.variant, handleEditorValueChange: onEditorValueChange, handleEditorUpdateBreakpoints: handleEditorUpdateBreakpoints diff --git a/src/pages/sourcecast/Sourcecast.tsx b/src/pages/sourcecast/Sourcecast.tsx index 88d65a4348..932ef44b27 100644 --- a/src/pages/sourcecast/Sourcecast.tsx +++ b/src/pages/sourcecast/Sourcecast.tsx @@ -2,7 +2,7 @@ import { Classes, Pre } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router'; import { @@ -12,6 +12,7 @@ import { debuggerResume } from 'src/commons/application/actions/InterpreterActions'; import { Position } from 'src/commons/editor/EditorTypes'; +import { useEditorState, useRepl,useSideContent,useWorkspace } from 'src/commons/redux/workspace/Hooks'; import { useResponsive, useTypedSelector } from 'src/commons/utils/Hooks'; import { browseReplHistoryDown, @@ -76,6 +77,13 @@ const Sourcecast: React.FC = () => { const { isMobileBreakpoint } = useResponsive(); const params = useParams<{ sourcecastId: string }>(); + const { + editorTabs, + activeEditorTabIndex, + } = useEditorState(workspaceLocation) + + const { replValue } = useRepl(workspaceLocation) + // Handlers migrated over from deprecated withRouter implementation const { audioUrl, @@ -83,7 +91,7 @@ const Sourcecast: React.FC = () => { codeDeltasToApply, title, description, - externalLibrary: externalLibraryName, + // externalLibrary: externalLibraryName, isEditorAutorun, isEditorReadonly, inputToApply, @@ -93,15 +101,12 @@ const Sourcecast: React.FC = () => { playbackDuration, playbackData, playbackStatus, - replValue, - sideContentHeight, sourcecastIndex, context: { chapter: sourceChapter, variant: sourceVariant }, uid, isFolderModeEnabled, - activeEditorTabIndex, - editorTabs - } = useTypedSelector(store => store.workspaces[workspaceLocation]); + } = useWorkspace(workspaceLocation); + const courseId = useTypedSelector(store => store.session.courseId); const dispatch = useDispatch(); @@ -151,7 +156,8 @@ const Sourcecast: React.FC = () => { * which contains the ag-grid table of available Sourcecasts. This is intentional * to avoid an ag-grid console warning. For more info, see issue #1152 in frontend. */ - const [selectedTab, setSelectedTab] = useState(SideContentType.introduction); + const { selectedTab, setSelectedTab, height: sideContentHeight } = useSideContent(workspaceLocation, SideContentType.introduction) + // const [selectedTab, setSelectedTab] = useState(SideContentType.introduction); const handleQueryParam = () => { const newUid = params.sourcecastId; @@ -211,7 +217,7 @@ const Sourcecast: React.FC = () => { ) { setSelectedTab(SideContentType.introduction); } - }, [isMobileBreakpoint, selectedTab]); + }, [isMobileBreakpoint, selectedTab, setSelectedTab]); const autorunButtonHandlers = useMemo(() => { return { @@ -355,7 +361,7 @@ const Sourcecast: React.FC = () => { handleReplValueChange: replHandlers.handleReplValueChange, sourceChapter: sourceChapter, sourceVariant: sourceVariant, - externalLibrary: externalLibraryName, + // externalLibrary: externalLibraryName, replButtons: [evalButton, clearButton] }; @@ -391,7 +397,7 @@ const Sourcecast: React.FC = () => { mobileControlBarProps: { editorButtons: [autorunButtons, chapterSelectButton] }, - selectedTabId: selectedTab, + selectedTabId: selectedTab!, onChange: onChangeTabs, tabs: { beforeDynamicTabs: tabs, From 1cdf43fead5f82d7a0b22d92997ed0285808a339 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Fri, 22 Sep 2023 14:13:57 +0800 Subject: [PATCH 08/13] Update workspace base paths --- src/pages/fileSystem/createInBrowserFileSystem.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pages/fileSystem/createInBrowserFileSystem.ts b/src/pages/fileSystem/createInBrowserFileSystem.ts index 86ab66b937..6970eb024e 100644 --- a/src/pages/fileSystem/createInBrowserFileSystem.ts +++ b/src/pages/fileSystem/createInBrowserFileSystem.ts @@ -7,6 +7,8 @@ import { OverallState } from '../../commons/application/ApplicationTypes'; import { setInBrowserFileSystem } from '../../commons/fileSystem/FileSystemActions'; import { writeFileRecursively } from '../../commons/fileSystem/utils'; import { EditorTabState, WorkspaceManagerState } from '../../commons/workspace/WorkspaceTypes'; +import { SideContentLocation } from 'src/commons/redux/workspace/subReducers/SideContentRedux'; +import { isNonStoryWorkspaceLocation } from 'src/commons/redux/workspace/WorkspaceRedux'; /** * Maps workspaces to their file system base path. @@ -24,6 +26,15 @@ export const WORKSPACE_BASE_PATHS: Record = stories: '' // TODO: Investigate if stories workspace base path is needed }; +export const getWorkspaceBasePath = (location: SideContentLocation) => { + if (isNonStoryWorkspaceLocation(location)) { + return WORKSPACE_BASE_PATHS[location] + } + + const [, storyEnv] = location.split('.') + return `${WORKSPACE_BASE_PATHS.stories}/${storyEnv}` +} + export const createInBrowserFileSystem = (store: Store): Promise => { return new Promise((resolve, reject) => { configure( @@ -59,7 +70,7 @@ export const createInBrowserFileSystem = (store: Store): Promise[] = []; for (const [, workspaceState] of Object.entries(workspaceStates)) { - const editorTabs = workspaceState.editorTabs; + const editorTabs = workspaceState.editorState.editorTabs; promises.push(createFilesForEditorTabs(fileSystem, editorTabs)); } Promise.all(promises) From 019bf3025162e4729c5a6547826f7374315e7109 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Fri, 22 Sep 2023 14:14:26 +0800 Subject: [PATCH 09/13] Update main saga --- src/commons/sagas/MainSaga.ts | 2 +- src/features/github/GitHubUtils.tsx | 2 +- src/pages/localStorage.ts | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/commons/sagas/MainSaga.ts b/src/commons/sagas/MainSaga.ts index cc7aaaf616..f2e9f5472c 100644 --- a/src/commons/sagas/MainSaga.ts +++ b/src/commons/sagas/MainSaga.ts @@ -2,13 +2,13 @@ import { SagaIterator } from 'redux-saga'; import { fork } from 'redux-saga/effects'; import { mockBackendSaga } from '../mocks/BackendMocks'; +import { PlaygroundSaga } from '../redux/workspace/playground/PlaygroundRedux' import Constants from '../utils/Constants'; import AchievementSaga from './AchievementSaga'; import BackendSaga from './BackendSaga'; import GitHubPersistenceSaga from './GitHubPersistenceSaga'; import LoginSaga from './LoginSaga'; import PersistenceSaga from './PersistenceSaga'; -import PlaygroundSaga from './PlaygroundSaga'; import RemoteExecutionSaga from './RemoteExecutionSaga'; import StoriesSaga from './StoriesSaga'; import WorkspaceSaga from './WorkspaceSaga'; diff --git a/src/features/github/GitHubUtils.tsx b/src/features/github/GitHubUtils.tsx index 21866465b4..164b8ed8a4 100644 --- a/src/features/github/GitHubUtils.tsx +++ b/src/features/github/GitHubUtils.tsx @@ -195,7 +195,7 @@ export async function openFileInEditor( if (content) { const newEditorValue = Buffer.from(content, 'base64').toString(); - const activeEditorTabIndex = store.getState().workspaces.playground.activeEditorTabIndex; + const activeEditorTabIndex = store.getState().workspaces.playground.editorState.activeEditorTabIndex; if (activeEditorTabIndex === null) { throw new Error('No active editor tab found.'); } diff --git a/src/pages/localStorage.ts b/src/pages/localStorage.ts index 8e3b36531d..0d1bdbf758 100644 --- a/src/pages/localStorage.ts +++ b/src/pages/localStorage.ts @@ -3,7 +3,6 @@ import { compressToUTF16, decompressFromUTF16 } from 'lz-string'; import { StoriesAuthState } from 'src/features/stories/StoriesTypes'; import { OverallState, SALanguage } from '../commons/application/ApplicationTypes'; -import { ExternalLibraryName } from '../commons/application/types/ExternalTypes'; import { SessionState } from '../commons/application/types/SessionTypes'; import { showWarningMessage } from '../commons/utils/notifications/NotificationsHelper'; import { EditorTabState } from '../commons/workspace/WorkspaceTypes'; @@ -28,7 +27,6 @@ export type SavedState = { playgroundSourceChapter: Chapter; playgroundSourceVariant: Variant; playgroundLanguage: SALanguage; - playgroundExternalLibrary: ExternalLibraryName; stories: Partial; }; @@ -77,14 +75,13 @@ export const saveState = (state: OverallState) => { achievements: state.achievement.achievements, playgroundIsFolderModeEnabled: state.workspaces.playground.isFolderModeEnabled, playgroundActiveEditorTabIndex: { - value: state.workspaces.playground.activeEditorTabIndex + value: state.workspaces.playground.editorState.activeEditorTabIndex }, - playgroundEditorTabs: state.workspaces.playground.editorTabs, + playgroundEditorTabs: state.workspaces.playground.editorState.editorTabs, playgroundIsEditorAutorun: state.workspaces.playground.isEditorAutorun, playgroundSourceChapter: state.workspaces.playground.context.chapter, playgroundSourceVariant: state.workspaces.playground.context.variant, playgroundLanguage: state.playground.languageConfig, - playgroundExternalLibrary: state.workspaces.playground.externalLibrary, stories: { userId: state.stories.userId, groupId: state.stories.groupId, From 510145cbcab52f103a7fd9b54214d8a2a6e7332c Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Fri, 22 Sep 2023 14:20:30 +0800 Subject: [PATCH 10/13] Update workspacemanager --- src/commons/application/ApplicationTypes.ts | 200 +++++++++--------- .../AssessmentWorkspace.tsx | 5 +- 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 1817a8384f..76e86e0cb1 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -5,17 +5,17 @@ import { AchievementState } from '../../features/achievement/AchievementTypes'; import { DashboardState } from '../../features/dashboard/DashboardTypes'; import { Grading } from '../../features/grading/GradingTypes'; import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; -import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes'; +// import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes'; import { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes'; import { WORKSPACE_BASE_PATHS } from '../../pages/fileSystem/createInBrowserFileSystem'; import { Assessment } from '../assessment/AssessmentTypes'; import { FileSystemState } from '../fileSystem/FileSystemTypes'; +import { defaultWorkspaceManager as newDefaultWorkspaceManager, WorkspaceManagerState } from '../redux/workspace/AllWorkspacesRedux'; import Constants from '../utils/Constants'; import { createContext } from '../utils/JsSlangHelper'; import { DebuggerContext, WorkspaceLocation, - WorkspaceManagerState, WorkspaceState } from '../workspace/WorkspaceTypes'; import { RouterState } from './types/CommonsTypes'; @@ -399,103 +399,103 @@ const defaultFileName = 'program.js'; export const getDefaultFilePath = (workspaceLocation: WorkspaceLocation) => `${WORKSPACE_BASE_PATHS[workspaceLocation]}/${defaultFileName}`; -export const defaultWorkspaceManager: WorkspaceManagerState = { - assessment: { - ...createDefaultWorkspace('assessment'), - currentAssessment: undefined, - currentQuestion: undefined, - hasUnsavedChanges: false - }, - grading: { - ...createDefaultWorkspace('grading'), - submissionsTableFilters: { - columnFilters: [], - globalFilter: null - }, - currentSubmission: undefined, - currentQuestion: undefined, - hasUnsavedChanges: false - }, - playground: { - ...createDefaultWorkspace('playground'), - usingSubst: false, - usingEnv: false, - updateEnv: true, - envSteps: -1, - envStepsTotal: 0, - breakpointSteps: [], - activeEditorTabIndex: 0, - editorTabs: [ - { - filePath: getDefaultFilePath('playground'), - value: defaultEditorValue, - highlightedLines: [], - breakpoints: [] - } - ] - }, - sourcecast: { - ...createDefaultWorkspace('sourcecast'), - audioUrl: '', - codeDeltasToApply: null, - currentPlayerTime: 0, - description: null, - inputToApply: null, - playbackData: { - init: { - editorValue: '', - chapter: Chapter.SOURCE_1, - externalLibrary: ExternalLibraryName.NONE - }, - inputs: [] - }, - playbackDuration: 0, - playbackStatus: PlaybackStatus.paused, - sourcecastIndex: null, - title: null, - uid: null - }, - sourcereel: { - ...createDefaultWorkspace('sourcereel'), - playbackData: { - init: { - editorValue: '', - chapter: Chapter.SOURCE_1, - externalLibrary: ExternalLibraryName.NONE - }, - inputs: [] - }, - recordingStatus: RecordingStatus.notStarted, - timeElapsedBeforePause: 0, - timeResumed: 0 - }, - sicp: { - ...createDefaultWorkspace('sicp'), - usingSubst: false, - usingEnv: false, - updateEnv: true, - envSteps: -1, - envStepsTotal: 0, - breakpointSteps: [], - activeEditorTabIndex: 0, - editorTabs: [ - { - filePath: getDefaultFilePath('sicp'), - value: defaultEditorValue, - highlightedLines: [], - breakpoints: [] - } - ] - }, - githubAssessment: { - ...createDefaultWorkspace('githubAssessment'), - hasUnsavedChanges: false - }, - stories: { - ...createDefaultWorkspace('stories') - // TODO: Perhaps we can add default values? - } -}; +// export const defaultWorkspaceManager: WorkspaceManagerState2 = { +// assessment: { +// ...createDefaultWorkspace('assessment'), +// currentAssessment: undefined, +// currentQuestion: undefined, +// hasUnsavedChanges: false +// }, +// grading: { +// ...createDefaultWorkspace('grading'), +// submissionsTableFilters: { +// columnFilters: [], +// globalFilter: null +// }, +// currentSubmission: undefined, +// currentQuestion: undefined, +// hasUnsavedChanges: false +// }, +// playground: { +// ...createDefaultWorkspace('playground'), +// usingSubst: false, +// usingEnv: false, +// updateEnv: true, +// envSteps: -1, +// envStepsTotal: 0, +// breakpointSteps: [], +// activeEditorTabIndex: 0, +// editorTabs: [ +// { +// filePath: getDefaultFilePath('playground'), +// value: defaultEditorValue, +// highlightedLines: [], +// breakpoints: [] +// } +// ] +// }, +// sourcecast: { +// ...createDefaultWorkspace('sourcecast'), +// audioUrl: '', +// codeDeltasToApply: null, +// currentPlayerTime: 0, +// description: null, +// inputToApply: null, +// playbackData: { +// init: { +// editorValue: '', +// chapter: Chapter.SOURCE_1, +// externalLibrary: ExternalLibraryName.NONE +// }, +// inputs: [] +// }, +// playbackDuration: 0, +// playbackStatus: PlaybackStatus.paused, +// sourcecastIndex: null, +// title: null, +// uid: null +// }, +// sourcereel: { +// ...createDefaultWorkspace('sourcereel'), +// playbackData: { +// init: { +// editorValue: '', +// chapter: Chapter.SOURCE_1, +// externalLibrary: ExternalLibraryName.NONE +// }, +// inputs: [] +// }, +// recordingStatus: RecordingStatus.notStarted, +// timeElapsedBeforePause: 0, +// timeResumed: 0 +// }, +// sicp: { +// ...createDefaultWorkspace('sicp'), +// usingSubst: false, +// usingEnv: false, +// updateEnv: true, +// envSteps: -1, +// envStepsTotal: 0, +// breakpointSteps: [], +// activeEditorTabIndex: 0, +// editorTabs: [ +// { +// filePath: getDefaultFilePath('sicp'), +// value: defaultEditorValue, +// highlightedLines: [], +// breakpoints: [] +// } +// ] +// }, +// githubAssessment: { +// ...createDefaultWorkspace('githubAssessment'), +// hasUnsavedChanges: false +// }, +// stories: { +// ...createDefaultWorkspace('stories') +// // TODO: Perhaps we can add default values? +// } +// }; export const defaultSession: SessionState = { courses: [], @@ -555,6 +555,6 @@ export const defaultState: OverallState = { playground: defaultPlayground, session: defaultSession, stories: defaultStories, - workspaces: defaultWorkspaceManager, + workspaces: newDefaultWorkspaceManager, fileSystem: defaultFileSystem }; diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 50c474b28d..3bfc361f33 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -27,7 +27,7 @@ import { SelectionRange } from '../../features/sourceRecorder/SourceRecorderTypes'; import { fetchAssessment, submitAnswer } from '../application/actions/SessionActions'; -import { defaultWorkspaceManager } from '../application/ApplicationTypes'; +// import { defaultWorkspaceManager } from '../application/ApplicationTypes'; import { AssessmentConfiguration, AutogradingResult, @@ -58,6 +58,7 @@ import { Position } from '../editor/EditorTypes'; import Markdown from '../Markdown'; import { MobileSideContentProps } from '../mobileWorkspace/mobileSideContent/MobileSideContent'; import MobileWorkspace, { MobileWorkspaceProps } from '../mobileWorkspace/MobileWorkspace'; +import { defaultAssessment } from '../redux/workspace/assessment/AssessmentRedux'; import { useEditorState, useRepl, useSideContent, useWorkspace } from '../redux/workspace/Hooks'; import SideContentAutograder from '../sideContent/content/SideContentAutograder'; import SideContentContestLeaderboard from '../sideContent/content/SideContentContestLeaderboard'; @@ -373,7 +374,7 @@ const AssessmentWorkspace: React.FC = props => { }); handleResetWorkspace(resetWorkspaceOptions); handleChangeExecTime( - question.library.execTimeMs ?? defaultWorkspaceManager.assessment.execTime + question.library.execTimeMs ?? defaultAssessment.execTime ); handleClearContext(question.library, true); handleUpdateHasUnsavedChanges(false); From bbf4f090e6468bc6bdbeb92c378c2cb620789dbb Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sun, 24 Sep 2023 23:20:59 +0800 Subject: [PATCH 11/13] Next stuff --- src/commons/XMLParser/XMLParserHelper.ts | 7 +- src/commons/application/ApplicationReducer.ts | 10 - src/commons/application/ApplicationTypes.ts | 251 +-- .../__tests__/ApplicationReducer.ts | 4 +- .../application/actions/InterpreterActions.ts | 56 - .../application/actions/SessionActions.ts | 92 -- .../application/reducers/CommonsReducer.ts | 17 - .../application/reducers/RootReducer.ts | 29 - .../application/reducers/SessionsReducer.ts | 153 -- .../reducers/__tests__/SessionReducer.ts | 2 +- src/commons/assessment/AssessmentTypes.ts | 8 +- .../AssessmentWorkspace.tsx | 180 +-- .../collabEditing/CollabEditingActions.ts | 19 - .../controlBar/ControlBarChapterSelect.tsx | 2 +- .../editingWorkspace/EditingWorkspace.tsx | 214 +-- ...itingWorkspaceSideContentDeploymentTab.tsx | 100 +- ...eContentProgrammingQuestionTemplateTab.tsx | 2 +- src/commons/editor/EditorContainer.tsx | 2 +- src/commons/editor/UseNavigation.tsx | 33 +- src/commons/fileSystem/utils.ts | 26 +- src/commons/fileSystemView/FileSystemView.tsx | 4 +- .../FileSystemViewDirectoryNode.tsx | 8 +- .../fileSystemView/FileSystemViewFileName.tsx | 13 +- .../fileSystemView/FileSystemViewFileNode.tsx | 10 +- .../fileSystemView/FileSystemViewList.tsx | 4 +- .../missionCreator/MissionCreatorContainer.ts | 4 +- .../mobileWorkspace/MobileWorkspace.tsx | 12 +- .../mobileSideContent/MobileSideContent.tsx | 54 +- src/commons/mocks/AssessmentMocks.ts | 2 - src/commons/mocks/BackendMocks.ts | 23 +- src/commons/mocks/StoreMocks.ts | 23 +- .../NavigationBarLangSelectButton.tsx | 14 +- src/commons/redux/ActionsHelper.ts | 27 + src/commons/redux/AllTypes.ts | 48 + src/commons/redux/ApplicationRedux.ts | 19 + src/commons/redux/DashboardRedux.ts | 14 + .../RemoteExecRedux.ts} | 282 ++-- src/commons/redux/RootReducer.ts | 24 + src/commons/redux/RouterReducer.ts | 15 + src/commons/redux/academy/AcademyReducer.ts | 41 + .../redux/achievement/AchievementReducer.ts | 62 + .../redux/achievement/AchievementSaga.ts | 130 ++ src/commons/redux/session/SessionsReducer.ts | 190 +++ .../{workspace => stories}/StoriesRedux.ts | 67 +- src/commons/redux/stories/StoriesSaga.ts | 139 ++ src/commons/redux/utils.ts | 144 +- src/commons/redux/utils/Selectors.ts | 27 + .../redux/workspace/AllWorkspacesRedux.ts | 91 +- src/commons/redux/workspace/Hooks.ts | 89 +- .../redux/workspace/NewWorkspaceSaga.ts | 621 ++++++-- src/commons/redux/workspace/SicpRedux.ts | 12 +- .../redux/workspace/SourcereelRedux.ts | 60 - src/commons/redux/workspace/WorkspaceRedux.ts | 117 +- .../redux/workspace/WorkspaceReduxTypes.ts | 378 +++++ .../workspace/assessment/AssessmentBase.ts | 45 +- .../workspace/assessment/AssessmentRedux.ts | 27 +- .../assessment/GithubAssesmentRedux.ts | 17 +- .../workspace/assessment/GradingRedux.ts | 24 +- .../workspace/playground/PlaygroundBase.ts | 59 +- .../workspace/playground/PlaygroundRedux.ts | 169 +-- .../workspace/playground}/PlaygroundSaga.ts | 152 +- .../{ => sourceRecorder}/SourcecastRedux.ts | 53 +- .../sourceRecorder/SourcereelRedux.ts | 65 + .../workspace/subReducers/EditorRedux.ts | 155 +- .../redux/workspace/subReducers/ReplRedux.ts | 56 +- .../workspace/subReducers/SideContentRedux.ts | 47 +- src/commons/repl/Repl.tsx | 9 +- src/commons/repl/ReplInput.tsx | 30 +- src/commons/sagas/AchievementSaga.ts | 2 +- src/commons/sagas/BackendSaga.ts | 57 +- src/commons/sagas/GitHubPersistenceSaga.ts | 43 +- src/commons/sagas/MainSaga.ts | 8 +- src/commons/sagas/PersistenceSaga.tsx | 82 +- src/commons/sagas/RequestsSaga.ts | 3 +- src/commons/sagas/SideContentSaga.ts | 11 +- src/commons/sagas/StoriesSaga.ts | 153 -- src/commons/sagas/WorkspaceSaga.ts | 1350 ----------------- src/commons/sagas/__tests__/PlaygroundSaga.ts | 152 +- src/commons/sagas/__tests__/WorkspaceSaga.ts | 487 +++--- .../sideContent/GenericSideContent.tsx | 108 -- src/commons/sideContent/SideContent.tsx | 64 +- src/commons/sideContent/SideContentHelper.ts | 27 +- .../sideContent/SideContentProvider.tsx | 5 +- src/commons/sideContent/SideContentTypes.ts | 8 +- .../content/SideContentAutograder.tsx | 4 +- .../content/SideContentDataVisualizer.tsx | 27 +- .../content/SideContentEnvVisualizer.tsx | 66 +- .../content/SideContentTestcaseCard.tsx | 7 +- .../SideContentRemoteExecution.tsx | 21 +- .../SourceRecorderControlBar.tsx | 3 - src/commons/utils/ActionsHelper.ts | 28 +- src/commons/utils/CastBackend.ts | 3 +- src/commons/utils/DisplayBufferService.ts | 10 +- src/commons/utils/Hooks.ts | 2 +- src/commons/workspace/Workspace.tsx | 13 +- src/commons/workspace/WorkspaceActions.ts | 412 ----- src/commons/workspace/WorkspaceReducer.ts | 1115 -------------- src/commons/workspace/WorkspaceTypes.ts | 162 -- .../workspace/__tests__/WorkspaceReducer.ts | 40 +- src/features/academy/AcademyReducer.ts | 23 - .../achievement/AchievementReducer.ts | 41 - .../__tests__/AchievementReducer.ts | 2 +- src/features/dashboard/DashboardReducer.ts | 20 - src/features/github/GitHubUtils.tsx | 8 +- .../githubAssessment/GitHubAssessmentTypes.ts | 7 - src/features/playground/PlaygroundReducer.ts | 47 - .../remoteExecution/RemoteExecutionActions.ts | 4 +- .../remoteExecution/RemoteExecutionTypes.ts | 4 +- .../sourceRecorder/SourceRecorderActions.ts | 84 - .../sourceRecorder/SourceRecorderTypes.ts | 3 - .../sourcecast/SourcecastActions.ts | 19 - .../sourcecast/SourcecastReducer.ts | 69 - .../sourcecast/SourcecastTypes.ts | 24 - .../sourcecast/__tests__/SourcecastReducer.ts | 31 +- .../sourcereel/SourcereelActions.ts | 69 - .../sourcereel/SourcereelReducer.ts | 80 - .../sourcereel/SourcereelTypes.ts | 16 +- .../sourcereel/__tests__/SourcereelReducer.ts | 42 +- src/features/stories/StoriesActions.ts | 2 +- src/features/stories/StoriesReducer.ts | 233 --- src/features/stories/StoriesTypes.ts | 28 +- .../storiesComponents/BackendAccess.ts | 5 +- .../stories/storiesComponents/SourceBlock.tsx | 64 +- .../storiesComponents/StoriesSideContent.tsx | 93 -- src/pages/__tests__/createStore.test.ts | 37 +- src/pages/__tests__/localStorage.test.ts | 5 +- src/pages/academy/adminPanel/AdminPanel.tsx | 4 +- .../subcomponents/AddStoriesUserPanel.tsx | 5 +- .../subcomponents/GradingSubmissionsTable.tsx | 4 +- .../subcomponents/GradingWorkspace.tsx | 213 +-- .../groundControl/GroundControlContainer.ts | 2 +- .../DefaultChapterSelectContainer.ts | 6 +- src/pages/academy/sourcereel/Sourcereel.tsx | 275 ++-- .../control/AchievementControlContainer.ts | 23 +- .../AchievementDashboardContainer.ts | 10 +- src/pages/createStore.ts | 16 +- .../fileSystem/createInBrowserFileSystem.ts | 21 +- .../GitHubAssessmentWorkspace.tsx | 145 +- src/pages/localStorage.ts | 17 +- src/pages/playground/Playground.tsx | 356 ++--- src/pages/playground/PlaygroundTabs.tsx | 10 +- src/pages/playground/__tests__/Playground.tsx | 2 +- src/pages/sicp/Sicp.tsx | 11 +- src/pages/sourcecast/Sourcecast.tsx | 204 +-- src/pages/stories/Stories.tsx | 10 +- src/pages/stories/Story.tsx | 2 +- 146 files changed, 3973 insertions(+), 7779 deletions(-) delete mode 100644 src/commons/application/ApplicationReducer.ts delete mode 100644 src/commons/application/actions/InterpreterActions.ts delete mode 100644 src/commons/application/reducers/CommonsReducer.ts delete mode 100644 src/commons/application/reducers/RootReducer.ts delete mode 100644 src/commons/application/reducers/SessionsReducer.ts delete mode 100644 src/commons/collabEditing/CollabEditingActions.ts create mode 100644 src/commons/redux/ActionsHelper.ts create mode 100644 src/commons/redux/AllTypes.ts create mode 100644 src/commons/redux/DashboardRedux.ts rename src/commons/{sagas/RemoteExecutionSaga.ts => redux/RemoteExecRedux.ts} (59%) create mode 100644 src/commons/redux/RootReducer.ts create mode 100644 src/commons/redux/RouterReducer.ts create mode 100644 src/commons/redux/academy/AcademyReducer.ts create mode 100644 src/commons/redux/achievement/AchievementReducer.ts create mode 100644 src/commons/redux/achievement/AchievementSaga.ts create mode 100644 src/commons/redux/session/SessionsReducer.ts rename src/commons/redux/{workspace => stories}/StoriesRedux.ts (58%) create mode 100644 src/commons/redux/stories/StoriesSaga.ts create mode 100644 src/commons/redux/utils/Selectors.ts delete mode 100644 src/commons/redux/workspace/SourcereelRedux.ts create mode 100644 src/commons/redux/workspace/WorkspaceReduxTypes.ts rename src/commons/{sagas => redux/workspace/playground}/PlaygroundSaga.ts (53%) rename src/commons/redux/workspace/{ => sourceRecorder}/SourcecastRedux.ts (63%) create mode 100644 src/commons/redux/workspace/sourceRecorder/SourcereelRedux.ts delete mode 100644 src/commons/sagas/StoriesSaga.ts delete mode 100644 src/commons/sagas/WorkspaceSaga.ts delete mode 100644 src/commons/sideContent/GenericSideContent.tsx delete mode 100644 src/commons/workspace/WorkspaceActions.ts delete mode 100644 src/commons/workspace/WorkspaceReducer.ts delete mode 100644 src/commons/workspace/WorkspaceTypes.ts delete mode 100644 src/features/academy/AcademyReducer.ts delete mode 100644 src/features/achievement/AchievementReducer.ts delete mode 100644 src/features/dashboard/DashboardReducer.ts delete mode 100644 src/features/githubAssessment/GitHubAssessmentTypes.ts delete mode 100644 src/features/playground/PlaygroundReducer.ts delete mode 100644 src/features/sourceRecorder/SourceRecorderActions.ts delete mode 100644 src/features/sourceRecorder/sourcecast/SourcecastActions.ts delete mode 100644 src/features/sourceRecorder/sourcecast/SourcecastReducer.ts delete mode 100644 src/features/sourceRecorder/sourcereel/SourcereelActions.ts delete mode 100644 src/features/sourceRecorder/sourcereel/SourcereelReducer.ts delete mode 100644 src/features/stories/StoriesReducer.ts delete mode 100644 src/features/stories/storiesComponents/StoriesSideContent.tsx diff --git a/src/commons/XMLParser/XMLParserHelper.ts b/src/commons/XMLParser/XMLParserHelper.ts index fbf9385575..c6985f8d41 100644 --- a/src/commons/XMLParser/XMLParserHelper.ts +++ b/src/commons/XMLParser/XMLParserHelper.ts @@ -1,7 +1,6 @@ import { Chapter } from 'js-slang/dist/types'; import { Builder } from 'xml2js'; -import { ExternalLibraryName } from '../application/types/ExternalTypes'; import { Assessment, AssessmentOverview, @@ -118,7 +117,7 @@ const makeLibrary = (deploymentArr: XmlParseStrDeployment[] | undefined): Librar } else { const deployment = deploymentArr[0]; const external = deployment.IMPORT || deployment.EXTERNAL; - const nameVal = external ? external[0].$.name : 'NONE'; + // const nameVal = external ? external[0].$.name : 'NONE'; const symbolsVal = external ? external[0].SYMBOL || [] : []; const globalsVal = deployment.GLOBAL ? (deployment.GLOBAL.map(x => [x.IDENTIFIER[0], altEval(x.VALUE[0]), x.VALUE[0]]) as Array< @@ -128,7 +127,7 @@ const makeLibrary = (deploymentArr: XmlParseStrDeployment[] | undefined): Librar return { chapter: parseInt(deployment.$.interpreter, 10) as Chapter, external: { - name: nameVal as ExternalLibraryName, + // name: nameVal as ExternalLibraryName, symbols: symbolsVal }, globals: globalsVal @@ -262,7 +261,7 @@ const exportLibrary = (library: Library) => { }, EXTERNAL: { $: { - name: library.external.name + // name: library.external.name } } }; diff --git a/src/commons/application/ApplicationReducer.ts b/src/commons/application/ApplicationReducer.ts deleted file mode 100644 index 548669192c..0000000000 --- a/src/commons/application/ApplicationReducer.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Action, Reducer } from 'redux'; - -import { ApplicationState, defaultApplication } from './ApplicationTypes'; - -export const ApplicationReducer: Reducer = ( - state = defaultApplication, - action: Action -) => { - return state; -}; diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 76e86e0cb1..52cefe02b2 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -1,43 +1,10 @@ import { Chapter, Language, SourceError, Variant } from 'js-slang/dist/types'; -import { AcademyState } from '../../features/academy/AcademyTypes'; -import { AchievementState } from '../../features/achievement/AchievementTypes'; -import { DashboardState } from '../../features/dashboard/DashboardTypes'; import { Grading } from '../../features/grading/GradingTypes'; -import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; -// import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes'; -import { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes'; -import { WORKSPACE_BASE_PATHS } from '../../pages/fileSystem/createInBrowserFileSystem'; import { Assessment } from '../assessment/AssessmentTypes'; import { FileSystemState } from '../fileSystem/FileSystemTypes'; -import { defaultWorkspaceManager as newDefaultWorkspaceManager, WorkspaceManagerState } from '../redux/workspace/AllWorkspacesRedux'; -import Constants from '../utils/Constants'; -import { createContext } from '../utils/JsSlangHelper'; -import { - DebuggerContext, - WorkspaceLocation, - WorkspaceState -} from '../workspace/WorkspaceTypes'; -import { RouterState } from './types/CommonsTypes'; -import { ExternalLibraryName } from './types/ExternalTypes'; import { SessionState } from './types/SessionTypes'; -export type OverallState = { - readonly router: RouterState; - readonly academy: AcademyState; - readonly achievement: AchievementState; - readonly application: ApplicationState; - readonly playground: PlaygroundState; - readonly session: SessionState; - readonly stories: StoriesState; - readonly workspaces: WorkspaceManagerState; - readonly dashboard: DashboardState; - readonly fileSystem: FileSystemState; -}; - -export type ApplicationState = { - readonly environment: ApplicationEnvironment; -}; export type Story = { story: string; @@ -95,11 +62,6 @@ export type ErrorOutput = { export type InterpreterOutput = RunningOutput | CodeOutput | ResultOutput | ErrorOutput; -export enum ApplicationEnvironment { - Development = 'development', - Production = 'production', - Test = 'test' -} export enum Role { Student = 'student', @@ -107,13 +69,6 @@ export enum Role { Admin = 'admin' } -// Must match https://github.com/source-academy/stories-backend/blob/main/internal/enums/groups/role.go -export enum StoriesRole { - Standard = 'member', - Moderator = 'moderator', - Admin = 'admin' -} - export enum SupportedLanguage { JAVASCRIPT = 'JavaScript', SCHEME = 'Scheme', @@ -288,116 +243,60 @@ export const getLanguageConfig = ( return languageConfig; }; -const currentEnvironment = (): ApplicationEnvironment => { - switch (process.env.NODE_ENV) { - case 'development': - return ApplicationEnvironment.Development; - case 'production': - return ApplicationEnvironment.Production; - default: - return ApplicationEnvironment.Test; - } -}; - -export const defaultRouter: RouterState = null; -export const defaultAcademy: AcademyState = { - gameCanvas: undefined -}; - -export const defaultApplication: ApplicationState = { - environment: currentEnvironment() -}; - -export const defaultDashboard: DashboardState = { - gradingSummary: { - cols: [], - rows: [] - } -}; - -export const defaultAchievement: AchievementState = { - achievements: [], - goals: [], - users: [], - assessmentOverviews: [] -}; - -const getDefaultLanguageConfig = (): SALanguage => { - const languageConfig = ALL_LANGUAGES.find( - sublang => - sublang.chapter === Constants.defaultSourceChapter && - sublang.variant === Constants.defaultSourceVariant - ); - if (!languageConfig) { - throw new Error('Cannot find language config to match default chapter and variant'); - } - return languageConfig; -}; -export const defaultLanguageConfig: SALanguage = getDefaultLanguageConfig(); +// /** +// * Create a default IWorkspaceState for 'resetting' a workspace. +// * Takes in parameters to set the js-slang library and chapter. +// * +// * @param workspaceLocation the location of the workspace, used for context +// */ +// export const createDefaultWorkspace = (workspaceLocation: WorkspaceLocation): WorkspaceState => ({ +// autogradingResults: [], +// context: createContext( +// Constants.defaultSourceChapter, +// [], +// workspaceLocation, +// Constants.defaultSourceVariant +// ), +// isFolderModeEnabled: false, +// activeEditorTabIndex: 0, +// editorTabs: [ +// { +// filePath: ['playground', 'sicp'].includes(workspaceLocation) +// ? getDefaultFilePath(workspaceLocation) +// : undefined, +// value: ['playground', 'sourcecast', 'githubAssessments'].includes(workspaceLocation) +// ? defaultEditorValue +// : '', +// highlightedLines: [], +// breakpoints: [] +// } +// ], +// programPrependValue: '', +// programPostpendValue: '', +// editorSessionId: '', +// isEditorReadonly: false, +// editorTestcases: [], +// externalLibrary: ExternalLibraryName.NONE, +// execTime: 1000, +// output: [], +// replHistory: { +// browseIndex: null, +// records: [], +// originalValue: '' +// }, +// replValue: '', +// sharedbConnected: false, +// stepLimit: 1000, +// globals: [], +// isEditorAutorun: false, +// isRunning: false, +// isDebugging: false, +// enableDebugging: true, +// debuggerContext: {} as DebuggerContext +// }); -export const defaultPlayground: PlaygroundState = { - githubSaveInfo: { repoName: '', filePath: '' }, - languageConfig: defaultLanguageConfig -}; -export const defaultEditorValue = '// Type your program in here!'; - -/** - * Create a default IWorkspaceState for 'resetting' a workspace. - * Takes in parameters to set the js-slang library and chapter. - * - * @param workspaceLocation the location of the workspace, used for context - */ -export const createDefaultWorkspace = (workspaceLocation: WorkspaceLocation): WorkspaceState => ({ - autogradingResults: [], - context: createContext( - Constants.defaultSourceChapter, - [], - workspaceLocation, - Constants.defaultSourceVariant - ), - isFolderModeEnabled: false, - activeEditorTabIndex: 0, - editorTabs: [ - { - filePath: ['playground', 'sicp'].includes(workspaceLocation) - ? getDefaultFilePath(workspaceLocation) - : undefined, - value: ['playground', 'sourcecast', 'githubAssessments'].includes(workspaceLocation) - ? defaultEditorValue - : '', - highlightedLines: [], - breakpoints: [] - } - ], - programPrependValue: '', - programPostpendValue: '', - editorSessionId: '', - isEditorReadonly: false, - editorTestcases: [], - externalLibrary: ExternalLibraryName.NONE, - execTime: 1000, - output: [], - replHistory: { - browseIndex: null, - records: [], - originalValue: '' - }, - replValue: '', - sharedbConnected: false, - stepLimit: 1000, - globals: [], - isEditorAutorun: false, - isRunning: false, - isDebugging: false, - enableDebugging: true, - debuggerContext: {} as DebuggerContext -}); - -const defaultFileName = 'program.js'; -export const getDefaultFilePath = (workspaceLocation: WorkspaceLocation) => - `${WORKSPACE_BASE_PATHS[workspaceLocation]}/${defaultFileName}`; // export const defaultWorkspaceManager: WorkspaceManagerState2 = { // assessment: { @@ -520,41 +419,21 @@ export const defaultSession: SessionState = { notifications: [] }; -export const defaultStories: StoriesState = { - storyList: [], - currentStoryId: null, - currentStory: null, - envs: {} -}; - -export const createDefaultStoriesEnv = ( - envName: string, - chapter: Chapter, - variant: Variant -): StoriesEnvState => ({ - context: createContext(chapter, [], envName, variant), - execTime: 1000, - isRunning: false, - output: [], - stepLimit: 1000, - globals: [], - usingSubst: false, - debuggerContext: {} as DebuggerContext -}); +// export const createDefaultStoriesEnv = ( +// envName: string, +// chapter: Chapter, +// variant: Variant +// ): StoriesEnvState => ({ +// context: createContext(chapter, [], envName, variant), +// execTime: 1000, +// isRunning: false, +// output: [], +// stepLimit: 1000, +// globals: [], +// usingSubst: false, +// debuggerContext: {} as DebuggerContext +// }); export const defaultFileSystem: FileSystemState = { inBrowserFileSystem: null }; - -export const defaultState: OverallState = { - router: defaultRouter, - academy: defaultAcademy, - achievement: defaultAchievement, - application: defaultApplication, - dashboard: defaultDashboard, - playground: defaultPlayground, - session: defaultSession, - stories: defaultStories, - workspaces: newDefaultWorkspaceManager, - fileSystem: defaultFileSystem -}; diff --git a/src/commons/application/__tests__/ApplicationReducer.ts b/src/commons/application/__tests__/ApplicationReducer.ts index 71ec053ba0..52d19ae22e 100644 --- a/src/commons/application/__tests__/ApplicationReducer.ts +++ b/src/commons/application/__tests__/ApplicationReducer.ts @@ -1,6 +1,6 @@ -import { ApplicationReducer } from '../ApplicationReducer'; // EDITED +import { applicationReducer } from '../../redux/ApplicationRedux' -const initialState = ApplicationReducer(undefined!, { type: '*' }); +const initialState = applicationReducer(undefined!, { type: '*' }); test('initial state should match a snapshot', () => { expect(initialState).toMatchSnapshot(); diff --git a/src/commons/application/actions/InterpreterActions.ts b/src/commons/application/actions/InterpreterActions.ts deleted file mode 100644 index 95c48356ef..0000000000 --- a/src/commons/application/actions/InterpreterActions.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { SourceError, Value } from 'js-slang/dist/types'; -import { action } from 'typesafe-actions'; - -import { WorkspaceLocation } from '../../workspace/WorkspaceTypes'; -import { - BEGIN_DEBUG_PAUSE, - BEGIN_INTERRUPT_EXECUTION, - DEBUG_RESET, - DEBUG_RESUME, - END_DEBUG_PAUSE, - END_INTERRUPT_EXECUTION, - EVAL_INTERPRETER_ERROR, - EVAL_INTERPRETER_SUCCESS, - EVAL_TESTCASE_FAILURE, - EVAL_TESTCASE_SUCCESS, - HANDLE_CONSOLE_LOG -} from '../types/InterpreterTypes'; - -export const handleConsoleLog = (workspaceLocation: WorkspaceLocation, ...logString: string[]) => - action(HANDLE_CONSOLE_LOG, { logString, workspaceLocation }); - -export const evalInterpreterSuccess = (value: Value, workspaceLocation: WorkspaceLocation) => - action(EVAL_INTERPRETER_SUCCESS, { type: 'result', value, workspaceLocation }); - -export const evalTestcaseSuccess = ( - value: Value, - workspaceLocation: WorkspaceLocation, - index: number -) => action(EVAL_TESTCASE_SUCCESS, { type: 'result', value, workspaceLocation, index }); - -export const evalTestcaseFailure = ( - value: Value, - workspaceLocation: WorkspaceLocation, - index: number -) => action(EVAL_TESTCASE_FAILURE, { type: 'errors', value, workspaceLocation, index }); - -export const evalInterpreterError = (errors: SourceError[], workspaceLocation: WorkspaceLocation) => - action(EVAL_INTERPRETER_ERROR, { type: 'errors', errors, workspaceLocation }); - -export const beginInterruptExecution = (workspaceLocation: WorkspaceLocation) => - action(BEGIN_INTERRUPT_EXECUTION, { workspaceLocation }); - -export const endInterruptExecution = (workspaceLocation: WorkspaceLocation) => - action(END_INTERRUPT_EXECUTION, { workspaceLocation }); - -export const beginDebuggerPause = (workspaceLocation: WorkspaceLocation) => - action(BEGIN_DEBUG_PAUSE, { workspaceLocation }); - -export const endDebuggerPause = (workspaceLocation: WorkspaceLocation) => - action(END_DEBUG_PAUSE, { workspaceLocation }); - -export const debuggerResume = (workspaceLocation: WorkspaceLocation) => - action(DEBUG_RESUME, { workspaceLocation }); - -export const debuggerReset = (workspaceLocation: WorkspaceLocation) => - action(DEBUG_RESET, { workspaceLocation }); diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index 3f86c93269..b1fb5b0b52 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -1,23 +1,16 @@ import { action } from 'typesafe-actions'; // EDITED -import { Grading, GradingOverview } from '../../../features/grading/GradingTypes'; import { - Assessment, AssessmentConfiguration, - AssessmentOverview, ContestEntry } from '../../assessment/AssessmentTypes'; -import { MissionRepoData } from '../../githubAssessments/GitHubMissionTypes'; import { Notification, NotificationFilterFunction } from '../../notificationBadge/NotificationBadgeTypes'; -import { generateOctokitInstance } from '../../utils/GitHubPersistenceHelper'; import { Role } from '../ApplicationTypes'; import { ACKNOWLEDGE_NOTIFICATIONS, - AdminPanelCourseRegistration, - CourseRegistration, DELETE_ASSESSMENT_CONFIG, DELETE_TIME_OPTIONS, DELETE_USER_COURSE_REGISTRATION, @@ -45,43 +38,22 @@ import { NotificationPreference, REAUTOGRADE_ANSWER, REAUTOGRADE_SUBMISSION, - REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, - SET_ADMIN_PANEL_COURSE_REGISTRATIONS, - SET_ASSESSMENT_CONFIGURATIONS, - SET_CONFIGURABLE_NOTIFICATION_CONFIGS, - SET_COURSE_CONFIGURATION, - SET_COURSE_REGISTRATION, - SET_GITHUB_ACCESS_TOKEN, - SET_GITHUB_ASSESSMENT, - SET_GITHUB_OCTOKIT_OBJECT, - SET_GOOGLE_USER, - SET_NOTIFICATION_CONFIGS, - SET_TOKENS, - SET_USER, SUBMIT_ANSWER, SUBMIT_ASSESSMENT, SUBMIT_GRADING, SUBMIT_GRADING_AND_CONTINUE, TimeOption, - Tokens, UNSUBMIT_SUBMISSION, - UPDATE_ALL_USER_XP, - UPDATE_ASSESSMENT, UPDATE_ASSESSMENT_CONFIGS, - UPDATE_ASSESSMENT_OVERVIEWS, UPDATE_COURSE_CONFIG, UPDATE_COURSE_RESEARCH_AGREEMENT, - UPDATE_GRADING, - UPDATE_GRADING_OVERVIEWS, UPDATE_LATEST_VIEWED_COURSE, UPDATE_NOTIFICATION_CONFIG, UPDATE_NOTIFICATION_PREFERENCES, UPDATE_NOTIFICATIONS, UPDATE_TIME_OPTIONS, - UPDATE_TOTAL_XP, UPDATE_USER_ROLE, UpdateCourseConfiguration, - User } from '../types/SessionTypes'; export const fetchAuth = (code: string, providerId?: string) => @@ -121,48 +93,6 @@ export const loginGitHub = () => action(LOGIN_GITHUB); export const logoutGitHub = () => action(LOGOUT_GITHUB); -export const setTokens = ({ accessToken, refreshToken }: Tokens) => - action(SET_TOKENS, { - accessToken, - refreshToken - }); - -export const setUser = (user: User) => action(SET_USER, user); - -export const setCourseConfiguration = (courseConfiguration: UpdateCourseConfiguration) => - action(SET_COURSE_CONFIGURATION, courseConfiguration); - -export const setCourseRegistration = (courseRegistration: Partial) => - action(SET_COURSE_REGISTRATION, courseRegistration); - -export const setAssessmentConfigurations = (assessmentConfigurations: AssessmentConfiguration[]) => - action(SET_ASSESSMENT_CONFIGURATIONS, assessmentConfigurations); - -export const setConfigurableNotificationConfigs = ( - notificationConfigs: NotificationConfiguration[] -) => action(SET_CONFIGURABLE_NOTIFICATION_CONFIGS, notificationConfigs); - -export const setNotificationConfigs = (notificationConfigs: NotificationConfiguration[]) => - action(SET_NOTIFICATION_CONFIGS, notificationConfigs); - -export const setAdminPanelCourseRegistrations = ( - courseRegistrations: AdminPanelCourseRegistration[] -) => action(SET_ADMIN_PANEL_COURSE_REGISTRATIONS, courseRegistrations); - -export const setGoogleUser = (user?: string) => action(SET_GOOGLE_USER, user); - -export const setGitHubAssessment = (missionRepoData: MissionRepoData) => - action(SET_GITHUB_ASSESSMENT, missionRepoData); - -export const setGitHubOctokitObject = (authToken?: string) => - action(SET_GITHUB_OCTOKIT_OBJECT, generateOctokitInstance(authToken || '')); - -export const setGitHubAccessToken = (authToken?: string) => - action(SET_GITHUB_ACCESS_TOKEN, authToken); - -export const removeGitHubOctokitObjectAndAccessToken = () => - action(REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN); - export const submitAnswer = (id: number, answer: string | number | ContestEntry[]) => action(SUBMIT_ANSWER, { id, @@ -203,28 +133,6 @@ export const reautogradeSubmission = (submissionId: number) => export const reautogradeAnswer = (submissionId: number, questionId: number) => action(REAUTOGRADE_ANSWER, { submissionId, questionId }); -export const updateAssessmentOverviews = (overviews: AssessmentOverview[]) => - action(UPDATE_ASSESSMENT_OVERVIEWS, overviews); - -export const updateTotalXp = (totalXp: number) => action(UPDATE_TOTAL_XP, totalXp); - -export const updateAllUserXp = (allUserXp: string[][]) => action(UPDATE_ALL_USER_XP, allUserXp); - -export const updateAssessment = (assessment: Assessment) => action(UPDATE_ASSESSMENT, assessment); - -export const updateGradingOverviews = (overviews: GradingOverview[]) => - action(UPDATE_GRADING_OVERVIEWS, overviews); - -/** - * An extra id parameter is included here because of - * no id for Grading. - */ -export const updateGrading = (submissionId: number, grading: Grading) => - action(UPDATE_GRADING, { - submissionId, - grading - }); - export const unsubmitSubmission = (submissionId: number) => action(UNSUBMIT_SUBMISSION, { submissionId diff --git a/src/commons/application/reducers/CommonsReducer.ts b/src/commons/application/reducers/CommonsReducer.ts deleted file mode 100644 index e992c747dd..0000000000 --- a/src/commons/application/reducers/CommonsReducer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Reducer } from 'redux'; -import { SourceActionType } from 'src/commons/utils/ActionsHelper'; - -import { defaultRouter } from '../ApplicationTypes'; -import { RouterState, UPDATE_REACT_ROUTER } from '../types/CommonsTypes'; - -export const RouterReducer: Reducer = ( - state = defaultRouter, - action: SourceActionType -) => { - switch (action.type) { - case UPDATE_REACT_ROUTER: - return action.payload; - default: - return state; - } -}; diff --git a/src/commons/application/reducers/RootReducer.ts b/src/commons/application/reducers/RootReducer.ts deleted file mode 100644 index d24563e998..0000000000 --- a/src/commons/application/reducers/RootReducer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { combineReducers } from 'redux'; -// import { WorkspaceReducer as workspaces } from '../../workspace/WorkspaceReducer'; -import { allWorkspacesReducer as workspaces } from 'src/commons/redux/workspace/AllWorkspacesRedux'; - -import { AcademyReducer as academy } from '../../../features/academy/AcademyReducer'; -import { AchievementReducer as achievement } from '../../../features/achievement/AchievementReducer'; -import { DashboardReducer as dashboard } from '../../../features/dashboard/DashboardReducer'; -import { PlaygroundReducer as playground } from '../../../features/playground/PlaygroundReducer'; -import { StoriesReducer as stories } from '../../../features/stories/StoriesReducer'; -import { FileSystemReducer as fileSystem } from '../../fileSystem/FileSystemReducer'; -import { ApplicationReducer as application } from '../ApplicationReducer'; -import { RouterReducer as router } from './CommonsReducer'; -import { SessionsReducer as session } from './SessionsReducer'; - -const createRootReducer = () => - combineReducers({ - router, - academy, - achievement, - application, - dashboard, - playground, - session, - stories, - workspaces, - fileSystem - }); - -export default createRootReducer; diff --git a/src/commons/application/reducers/SessionsReducer.ts b/src/commons/application/reducers/SessionsReducer.ts deleted file mode 100644 index 32491eadd6..0000000000 --- a/src/commons/application/reducers/SessionsReducer.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Reducer } from 'redux'; - -import { - REMOTE_EXEC_UPDATE_DEVICES, - REMOTE_EXEC_UPDATE_SESSION -} from '../../../features/remoteExecution/RemoteExecutionTypes'; -import { SourceActionType } from '../../utils/ActionsHelper'; -import { defaultSession } from '../ApplicationTypes'; -import { LOG_OUT } from '../types/CommonsTypes'; -import { - REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, - SessionState, - SET_ADMIN_PANEL_COURSE_REGISTRATIONS, - SET_ASSESSMENT_CONFIGURATIONS, - SET_CONFIGURABLE_NOTIFICATION_CONFIGS, - SET_COURSE_CONFIGURATION, - SET_COURSE_REGISTRATION, - SET_GITHUB_ACCESS_TOKEN, - SET_GITHUB_ASSESSMENT, - SET_GITHUB_OCTOKIT_OBJECT, - SET_GOOGLE_USER, - SET_NOTIFICATION_CONFIGS, - SET_TOKENS, - SET_USER, - UPDATE_ALL_USER_XP, - UPDATE_ASSESSMENT, - UPDATE_ASSESSMENT_OVERVIEWS, - UPDATE_GRADING, - UPDATE_GRADING_OVERVIEWS, - UPDATE_NOTIFICATIONS, - UPDATE_TOTAL_XP -} from '../types/SessionTypes'; - -export const SessionsReducer: Reducer = ( - state = defaultSession, - action: SourceActionType -) => { - switch (action.type) { - case LOG_OUT: - return defaultSession; - case SET_GITHUB_ASSESSMENT: - return { - ...state, - githubAssessment: action.payload - }; - case SET_GITHUB_OCTOKIT_OBJECT: - return { - ...state, - githubOctokitObject: { octokit: action.payload } - }; - case SET_GITHUB_ACCESS_TOKEN: - return { - ...state, - githubAccessToken: action.payload - }; - case SET_GOOGLE_USER: - return { - ...state, - googleUser: action.payload - }; - case SET_TOKENS: - return { - ...state, - ...action.payload - }; - case SET_USER: - return { - ...state, - ...action.payload - }; - case SET_COURSE_CONFIGURATION: - return { - ...state, - ...action.payload - }; - case SET_COURSE_REGISTRATION: - return { - ...state, - ...action.payload - }; - case SET_ASSESSMENT_CONFIGURATIONS: - return { - ...state, - assessmentConfigurations: action.payload - }; - case SET_NOTIFICATION_CONFIGS: - return { - ...state, - notificationConfigs: action.payload - }; - case SET_CONFIGURABLE_NOTIFICATION_CONFIGS: - return { - ...state, - configurableNotificationConfigs: action.payload - }; - case SET_ADMIN_PANEL_COURSE_REGISTRATIONS: - return { - ...state, - userCourseRegistrations: action.payload - }; - case UPDATE_ASSESSMENT: - const newAssessments = new Map(state.assessments); - newAssessments.set(action.payload.id, action.payload); - return { - ...state, - assessments: newAssessments - }; - case UPDATE_ASSESSMENT_OVERVIEWS: - return { - ...state, - assessmentOverviews: action.payload - }; - case UPDATE_TOTAL_XP: - return { ...state, xp: action.payload }; - case UPDATE_ALL_USER_XP: - return { ...state, allUserXp: action.payload }; - case UPDATE_GRADING: - const newGradings = new Map(state.gradings); - newGradings.set(action.payload.submissionId, action.payload.grading); - return { - ...state, - gradings: newGradings - }; - case UPDATE_GRADING_OVERVIEWS: - return { - ...state, - gradingOverviews: action.payload - }; - case UPDATE_NOTIFICATIONS: - return { - ...state, - notifications: action.payload - }; - case REMOTE_EXEC_UPDATE_DEVICES: - return { - ...state, - remoteExecutionDevices: action.payload - }; - case REMOTE_EXEC_UPDATE_SESSION: - return { - ...state, - remoteExecutionSession: action.payload - }; - case REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN: - return { - ...state, - githubOctokitObject: { octokit: undefined }, - githubAccessToken: undefined - }; - default: - return state; - } -}; diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index cbfd2c0e52..5bb3814fa6 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -25,7 +25,7 @@ import { UPDATE_GRADING_OVERVIEWS, UPDATE_NOTIFICATIONS } from '../../types/SessionTypes'; -import { SessionsReducer } from '../SessionsReducer'; +import { SessionsReducer } from '../../../redux/session/SessionsReducer'; test('LOG_OUT works correctly on default session', () => { const action = { diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index 0169f52ad0..2452f1d43b 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -1,7 +1,5 @@ import { Chapter, SourceError, Variant } from 'js-slang/dist/types'; -import { ExternalLibrary, ExternalLibraryName } from '../application/types/ExternalTypes'; - export const FETCH_ASSESSMENT_OVERVIEWS = 'FETCH_ASSESSMENT_OVERVIEWS'; export const SUBMIT_ASSESSMENT = 'SUBMIT_ASSESSMENT'; @@ -152,12 +150,14 @@ export type Library = { chapter: Chapter; variant?: Variant; execTimeMs?: number; - external: ExternalLibrary; globals: Array<{ 0: string; 1: any; 2?: string; // For mission control }>; + external: { + symbols: string[] + } moduleParams?: any; }; @@ -207,7 +207,6 @@ export const emptyLibrary = (): Library => { return { chapter: -1, external: { - name: 'NONE' as ExternalLibraryName, symbols: [] }, globals: [] @@ -218,7 +217,6 @@ export const normalLibrary = (): Library => { return { chapter: Chapter.SOURCE_1, external: { - name: 'NONE' as ExternalLibraryName, symbols: [] }, globals: [] diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 3bfc361f33..1b266b02b3 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -10,6 +10,7 @@ import { SpinnerSize } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import { DeepPartial } from '@reduxjs/toolkit'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; import { isEqual } from 'lodash'; @@ -27,7 +28,6 @@ import { SelectionRange } from '../../features/sourceRecorder/SourceRecorderTypes'; import { fetchAssessment, submitAnswer } from '../application/actions/SessionActions'; -// import { defaultWorkspaceManager } from '../application/ApplicationTypes'; import { AssessmentConfiguration, AutogradingResult, @@ -35,7 +35,6 @@ import { IContestVotingQuestion, IMCQQuestion, IProgrammingQuestion, - Library, QuestionTypes, Testcase } from '../assessment/AssessmentTypes'; @@ -54,12 +53,13 @@ import { convertEditorTabStateToProps, NormalEditorContainerProps } from '../editor/EditorContainer'; -import { Position } from '../editor/EditorTypes'; import Markdown from '../Markdown'; import { MobileSideContentProps } from '../mobileWorkspace/mobileSideContent/MobileSideContent'; import MobileWorkspace, { MobileWorkspaceProps } from '../mobileWorkspace/MobileWorkspace'; -import { defaultAssessment } from '../redux/workspace/assessment/AssessmentRedux'; +import { allWorkspaceActions } from '../redux/workspace/AllWorkspacesRedux'; import { useEditorState, useRepl, useSideContent, useWorkspace } from '../redux/workspace/Hooks'; +import { AssessmentWorkspaceState, defaultAssessment, SideContentLocation } from '../redux/workspace/WorkspaceReduxTypes'; +import { ReplProps } from '../repl/Repl'; import SideContentAutograder from '../sideContent/content/SideContentAutograder'; import SideContentContestLeaderboard from '../sideContent/content/SideContentContestLeaderboard'; import SideContentContestVotingContainer from '../sideContent/content/SideContentContestVotingContainer'; @@ -71,30 +71,8 @@ import { useResponsive, useTypedSelector } from '../utils/Hooks'; import { assessmentTypeLink } from '../utils/ParamParseHelper'; import { assertType } from '../utils/TypeHelper'; import Workspace, { WorkspaceProps } from '../workspace/Workspace'; -import { - beginClearContext, - browseReplHistoryDown, - browseReplHistoryUp, - changeExecTime, - changeSideContentHeight, - clearReplOutput, - evalEditor, - evalRepl, - evalTestcase, - navigateToDeclaration, - promptAutocomplete, - removeEditorTab, - resetWorkspace, - runAllTestcases, - setEditorBreakpoint, - updateActiveEditorTabIndex, - updateCurrentAssessmentId, - updateEditorValue, - updateHasUnsavedChanges, - updateReplValue -} from '../workspace/WorkspaceActions'; -import { WorkspaceLocation, WorkspaceState } from '../workspace/WorkspaceTypes'; import AssessmentWorkspaceGradingResult from './AssessmentWorkspaceGradingResult'; + export type AssessmentWorkspaceProps = { assessmentId: number; questionId: number; @@ -103,7 +81,7 @@ export type AssessmentWorkspaceProps = { assessmentConfiguration: AssessmentConfiguration; }; -const workspaceLocation: WorkspaceLocation = 'assessment'; +const workspaceLocation: SideContentLocation = 'assessment'; const AssessmentWorkspace: React.FC = props => { const [showOverlay, setShowOverlay] = useState(false); @@ -112,67 +90,67 @@ const AssessmentWorkspace: React.FC = props => { const { isMobileBreakpoint } = useResponsive(); const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); - const { selectedTab, setSelectedTab, height: sideContentHeight } = useSideContent(workspaceLocation, + const { selectedTab, setSelectedTab } = useSideContent(workspaceLocation, assessment?.questions[props.questionId].grader !== undefined ? SideContentType.grading : SideContentType.questionOverview ) - const { activeEditorTabIndex, editorTabs } = useEditorState(workspaceLocation) - const { replValue } = useRepl(workspaceLocation) + const { + activeEditorTabIndex, + editorTabs, + editorSessionId, + isEditorAutorun, + isFolderModeEnabled, + updateEditorBreakpoints: handleEditorUpdateBreakpoints, + updateEditorValue: handleEditorValueChange, + updateActiveEditorTabIndex: handleUpdateActiveEditorTabIndex, + removeEditorTab: handleRemoveEditorTabByIndex, + } = useEditorState(workspaceLocation) + + const { + clearReplOutput + } = useRepl(workspaceLocation) const navigate = useNavigate(); const { courseId } = useTypedSelector(state => state.session); const { - isFolderModeEnabled, autogradingResults, + context, editorTestcases, hasUnsavedChanges, isRunning, output, currentAssessment: storedAssessmentId, - currentQuestion: storedQuestionId + currentQuestion: storedQuestionId, + globals: workspaceGlobals, + beginClearContext: handleClearContext, + changeExecTime: handleChangeExecTime, + evalEditor: handleEditorEval, + evalRepl: handleReplEval, + navDeclaration: handleDeclarationNavigate, + promptAutocomplete: handlePromptAutocomplete, + resetWorkspace: handleResetWorkspace, + updateHasUnsavedChanges: handleUpdateHasUnsavedChanges } = useWorkspace(workspaceLocation) const dispatch = useDispatch(); const { handleTestcaseEval, - handleClearContext, - handleChangeExecTime, handleUpdateCurrentAssessmentId, - handleResetWorkspace, handleRunAllTestcases, - handleEditorEval, handleAssessmentFetch, - handleEditorValueChange, - handleEditorUpdateBreakpoints, - handleReplEval, handleSave, - handleUpdateHasUnsavedChanges } = useMemo(() => { return { - handleTestcaseEval: (id: number) => dispatch(evalTestcase(workspaceLocation, id)), - handleClearContext: (library: Library, shouldInitLibrary: boolean) => - dispatch(beginClearContext(workspaceLocation, library, shouldInitLibrary)), - handleChangeExecTime: (execTimeMs: number) => - dispatch(changeExecTime(execTimeMs, workspaceLocation)), + handleTestcaseEval: (id: number) => dispatch(allWorkspaceActions.evalTestCase(workspaceLocation, id)), handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => - dispatch(updateCurrentAssessmentId(assessmentId, questionId)), - handleResetWorkspace: (options: Partial) => - dispatch(resetWorkspace(workspaceLocation, options)), - handleRunAllTestcases: () => dispatch(runAllTestcases(workspaceLocation)), - handleEditorEval: () => dispatch(evalEditor(workspaceLocation)), + dispatch(allWorkspaceActions.updateCurrentAssessmentId(workspaceLocation, assessmentId, questionId)), + handleRunAllTestcases: () => dispatch(allWorkspaceActions.evalEditorAndTestcases(workspaceLocation)), handleAssessmentFetch: (assessmentId: number) => dispatch(fetchAssessment(assessmentId)), - handleEditorValueChange: (editorTabIndex: number, newEditorValue: string) => - dispatch(updateEditorValue(workspaceLocation, editorTabIndex, newEditorValue)), - handleEditorUpdateBreakpoints: (editorTabIndex: number, newBreakpoints: string[]) => - dispatch(setEditorBreakpoint(workspaceLocation, editorTabIndex, newBreakpoints)), - handleReplEval: () => dispatch(evalRepl(workspaceLocation)), handleSave: (id: number, answer: number | string | ContestEntry[]) => dispatch(submitAnswer(id, answer)), - handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => - dispatch(updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges)) }; }, [dispatch]); @@ -345,7 +323,6 @@ const AssessmentWorkspace: React.FC = props => { setSessionId( initSession(`${(assessment as any).number}/${props.questionId}`, { chapter: question.library.chapter, - externalLibrary: question?.library?.external?.name || 'NONE', editorValue: options.editorValue }) ); @@ -364,10 +341,12 @@ const AssessmentWorkspace: React.FC = props => { // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. handleEditorUpdateBreakpoints(0, []); handleUpdateCurrentAssessmentId(assessmentId, questionId); - const resetWorkspaceOptions = assertType()({ + const resetWorkspaceOptions = assertType>()({ autogradingResults: options.autogradingResults ?? [], - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - editorTabs: [{ value: options.editorValue ?? '', highlightedLines: [], breakpoints: [] }], + editorState: { + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + editorTabs: [{ value: options.editorValue ?? '', highlightedLines: [], breakpoints: [] }], + }, programPrependValue: options.programPrependValue ?? '', programPostpendValue: options.programPostpendValue ?? '', editorTestcases: options.editorTestcases ?? [] @@ -376,7 +355,7 @@ const AssessmentWorkspace: React.FC = props => { handleChangeExecTime( question.library.execTimeMs ?? defaultAssessment.execTime ); - handleClearContext(question.library, true); + handleClearContext(context.chapter, context.variant, workspaceGlobals, context.externalContext.symbols); handleUpdateHasUnsavedChanges(false); if (options.editorValue) { // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. @@ -519,7 +498,7 @@ const AssessmentWorkspace: React.FC = props => { afterDynamicTabs: [] }, onChange: onChangeTabs, - workspaceLocation: workspaceLocation + location: workspaceLocation }; }; @@ -658,7 +637,7 @@ const AssessmentWorkspace: React.FC = props => { const replButtons = useMemo(() => { const clearButton = ( dispatch(clearReplOutput(workspaceLocation))} + handleReplOutputClear={clearReplOutput} key="clear_repl" /> ); @@ -667,36 +646,7 @@ const AssessmentWorkspace: React.FC = props => { ); return [evalButton, clearButton]; - }, [dispatch, isRunning, handleReplEval]); - - const editorContainerHandlers = useMemo(() => { - return { - setActiveEditorTabIndex: (activeEditorTabIndex: number | null) => - dispatch(updateActiveEditorTabIndex(workspaceLocation, activeEditorTabIndex)), - removeEditorTabByIndex: (editorTabIndex: number) => - dispatch(removeEditorTab(workspaceLocation, editorTabIndex)), - handleDeclarationNavigate: (cursorPosition: Position) => - dispatch(navigateToDeclaration(workspaceLocation, cursorPosition)), - handlePromptAutocomplete: (row: number, col: number, callback: any) => - dispatch(promptAutocomplete(workspaceLocation, row, col, callback)) - }; - }, [dispatch]); - - const replHandlers = useMemo(() => { - return { - handleBrowseHistoryDown: () => dispatch(browseReplHistoryDown(workspaceLocation)), - handleBrowseHistoryUp: () => dispatch(browseReplHistoryUp(workspaceLocation)), - handleReplValueChange: (newValue: string) => - dispatch(updateReplValue(newValue, workspaceLocation)) - }; - }, [dispatch]); - - const workspaceHandlers = useMemo(() => { - return { - handleSideContentHeightChange: (heightChange: number) => - dispatch(changeSideContentHeight(heightChange, workspaceLocation)) - }; - }, [dispatch]); + }, [clearReplOutput, handleReplEval, isRunning]); /* =============== Rendering Logic @@ -768,42 +718,37 @@ const AssessmentWorkspace: React.FC = props => { const editorContainerProps: NormalEditorContainerProps | undefined = question.type === QuestionTypes.programming || question.type === QuestionTypes.voting ? { - editorVariant: 'normal', - isFolderModeEnabled, activeEditorTabIndex, - setActiveEditorTabIndex: editorContainerHandlers.setActiveEditorTabIndex, - removeEditorTabByIndex: editorContainerHandlers.removeEditorTabByIndex, + editorVariant: 'normal', + editorSessionId, editorTabs: editorTabs.map(convertEditorTabStateToProps), - editorSessionId: '', sourceChapter: question.library.chapter || Chapter.SOURCE_4, sourceVariant: question.library.variant ?? Variant.DEFAULT, - externalLibraryName: question.library.external.name || 'NONE', - handleDeclarationNavigate: editorContainerHandlers.handleDeclarationNavigate, - handleEditorEval: handleEval, - handleEditorValueChange: handleEditorValueChange, - handleUpdateHasUnsavedChanges: handleUpdateHasUnsavedChanges, - handleEditorUpdateBreakpoints: handleEditorUpdateBreakpoints, - handlePromptAutocomplete: editorContainerHandlers.handlePromptAutocomplete, - isEditorAutorun: false, + handleDeclarationNavigate, + handlePromptAutocomplete, onChange: onChangeMethod, onCursorChange: onCursorChangeMethod, - onSelectionChange: onSelectionChangeMethod + onSelectionChange: onSelectionChangeMethod, + handleEditorEval, + isEditorAutorun, + isFolderModeEnabled, + setActiveEditorTabIndex: handleUpdateActiveEditorTabIndex, + removeEditorTabByIndex: handleRemoveEditorTabByIndex, + // TODO check this + handleEditorUpdateBreakpoints, + handleEditorValueChange, } : undefined; const mcqProps = { mcq: question as IMCQQuestion, handleMCQSubmit: (option: number) => handleSave(assessment!.questions[questionId].id, option) }; - const replProps = { - handleBrowseHistoryDown: replHandlers.handleBrowseHistoryDown, - handleBrowseHistoryUp: replHandlers.handleBrowseHistoryUp, + const replProps: ReplProps = { handleReplEval: handleReplEval, - handleReplValueChange: replHandlers.handleReplValueChange, output: output, - replValue: replValue, + location: workspaceLocation, sourceChapter: question?.library?.chapter || Chapter.SOURCE_4, sourceVariant: question.library.variant ?? Variant.DEFAULT, - externalLibrary: question?.library?.external?.name || 'NONE', replButtons: replButtons }; const sideBarProps = { @@ -812,13 +757,12 @@ const AssessmentWorkspace: React.FC = props => { const workspaceProps: WorkspaceProps = { controlBarProps: controlBarProps(questionId), editorContainerProps: editorContainerProps, - handleSideContentHeightChange: workspaceHandlers.handleSideContentHeightChange, hasUnsavedChanges: hasUnsavedChanges, mcqProps: mcqProps, sideBarProps: sideBarProps, - sideContentHeight: sideContentHeight, sideContentProps: sideContentProps(props, questionId), - replProps: replProps + replProps, + workspaceLocation }; const mobileWorkspaceProps: MobileWorkspaceProps = { editorContainerProps: editorContainerProps, diff --git a/src/commons/collabEditing/CollabEditingActions.ts b/src/commons/collabEditing/CollabEditingActions.ts deleted file mode 100644 index f8a75beb55..0000000000 --- a/src/commons/collabEditing/CollabEditingActions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { action } from 'typesafe-actions'; // EDITING - -import { WorkspaceLocation } from '../workspace/WorkspaceTypes'; -import { SET_EDITOR_SESSION_ID, SET_SHAREDB_CONNECTED } from './CollabEditingTypes'; - -export const setEditorSessionId = (workspaceLocation: WorkspaceLocation, editorSessionId: string) => - action(SET_EDITOR_SESSION_ID, { - workspaceLocation, - editorSessionId - }); - -/** - * Sets ShareDB connection status. - * - * @param workspaceLocation the workspace to be reset - * @param connected whether we are connected to ShareDB - */ -export const setSharedbConnected = (workspaceLocation: WorkspaceLocation, connected: boolean) => - action(SET_SHAREDB_CONNECTED, { workspaceLocation, connected }); diff --git a/src/commons/controlBar/ControlBarChapterSelect.tsx b/src/commons/controlBar/ControlBarChapterSelect.tsx index fc611dca3f..62c7a52fd9 100644 --- a/src/commons/controlBar/ControlBarChapterSelect.tsx +++ b/src/commons/controlBar/ControlBarChapterSelect.tsx @@ -78,7 +78,7 @@ export const ControlBarChapterSelect: React.FC = ( handleChapterSelect = () => {}, disabled = false }) => { - const selectedLang = useTypedSelector(store => store.playground.languageConfig.mainLanguage); + const selectedLang = useTypedSelector(store => store.workspaces.playground.languageConfig.mainLanguage); const choices = [ ...sourceLanguages, diff --git a/src/commons/editingWorkspace/EditingWorkspace.tsx b/src/commons/editingWorkspace/EditingWorkspace.tsx index a98f4812e7..9aabfe6748 100644 --- a/src/commons/editingWorkspace/EditingWorkspace.tsx +++ b/src/commons/editingWorkspace/EditingWorkspace.tsx @@ -20,7 +20,6 @@ import { AssessmentOverview, IMCQQuestion, IProgrammingQuestion, - Library, Question, QuestionTypes, Testcase @@ -44,36 +43,14 @@ import MCQQuestionTemplateTab from '../editingWorkspaceSideContent/EditingWorksp import ProgrammingQuestionTemplateTab from '../editingWorkspaceSideContent/EditingWorkspaceSideContentProgrammingQuestionTemplateTab'; import { TextAreaContent } from '../editingWorkspaceSideContent/EditingWorkspaceSideContentTextAreaContent'; import { convertEditorTabStateToProps } from '../editor/EditorContainer'; -import { Position } from '../editor/EditorTypes'; import Markdown from '../Markdown'; -import { useEditorState, useRepl, useSideContent } from '../redux/workspace/Hooks'; +import { allWorkspaceActions } from '../redux/workspace/AllWorkspacesRedux'; +import { useEditorState, useRepl, useSideContent, useWorkspace } from '../redux/workspace/Hooks'; +import { SideContentLocation } from '../redux/workspace/WorkspaceReduxTypes'; import SideContentToneMatrix from '../sideContent/content/SideContentToneMatrix'; import { SideContentProps } from '../sideContent/SideContent'; import { SideContentTab, SideContentType } from '../sideContent/SideContentTypes'; -import { useTypedSelector } from '../utils/Hooks'; import Workspace, { WorkspaceProps } from '../workspace/Workspace'; -import { - beginClearContext, - browseReplHistoryDown, - browseReplHistoryUp, - changeSideContentHeight, - clearReplOutput, - evalEditor, - evalRepl, - evalTestcase, - navigateToDeclaration, - promptAutocomplete, - removeEditorTab, - resetWorkspace, - setEditorBreakpoint, - updateActiveEditorTabIndex, - updateCurrentAssessmentId, - updateEditorValue, - updateHasUnsavedChanges, - updateReplValue, - updateWorkspace -} from '../workspace/WorkspaceActions'; -import { WorkspaceLocation, WorkspaceState } from '../workspace/WorkspaceTypes'; import { retrieveLocalAssessment, storeLocalAssessment, @@ -89,7 +66,7 @@ export type EditingWorkspaceProps = { closeDate: string; }; -const workspaceLocation: WorkspaceLocation = 'assessment'; +const workspaceLocation: SideContentLocation = 'assessment'; const EditingWorkspace: React.FC = props => { const [assessment, setAssessment] = useState(retrieveLocalAssessment()); @@ -99,20 +76,39 @@ const EditingWorkspace: React.FC = props => { const [originalMaxXp, setOriginalMaxXp] = useState(0); const navigate = useNavigate(); - const { activeEditorTabIndex, editorTabs } = useEditorState(workspaceLocation) - const { replValue } = useRepl(workspaceLocation) - const { height: sideContentHeight } = useSideContent( + const { + activeEditorTabIndex, + editorTabs, + editorSessionId, + isEditorAutorun, + isFolderModeEnabled, + removeEditorTab: handleRemoveEditorTabByIndex, + updateActiveEditorTabIndex: handleUpdateActiveEditorTabIndex, + updateEditorValue: handleEditorValueChange, + updateEditorBreakpoints: handleEditorUpdateBreakpoints + } = useEditorState(workspaceLocation) + useSideContent( workspaceLocation, SideContentType.introduction ) const { - isFolderModeEnabled, + clearReplOutput: handleReplOutputClear, + } = useRepl(workspaceLocation) + + const { isRunning, output, currentAssessment: storedAssessmentId, - currentQuestion: storedQuestionId - } = useTypedSelector(store => store.workspaces[workspaceLocation]); + currentQuestion: storedQuestionId, + evalEditor: handleEditorEval, + evalRepl: handleReplEval, + navDeclaration: handleDeclarationNavigate, + promptAutocomplete: handlePromptAutocomplete, + resetWorkspace: handleResetWorkspace, + updateHasUnsavedChanges: handleUpdateHasUnsavedChanges, + updateWorkspace: handleUpdateWorkspace + } = useWorkspace(workspaceLocation) /** * After mounting (either an older copy of the assessment @@ -135,60 +131,14 @@ const EditingWorkspace: React.FC = props => { const dispatch = useDispatch(); const { - handleBrowseHistoryDown, - handleBrowseHistoryUp, - handleClearContext, - handleDeclarationNavigate, - handleEditorEval, - handleEditorValueChange, - handleEditorUpdateBreakpoints, - handleReplEval, - handleReplOutputClear, - handleReplValueChange, - handleResetWorkspace, - handleUpdateWorkspace, handleSubmitAnswer, - handleSideContentHeightChange, - handleUpdateHasUnsavedChanges, handleUpdateCurrentAssessmentId, - handlePromptAutocomplete, - setActiveEditorTabIndex, - removeEditorTabByIndex } = useMemo(() => { return { - handleBrowseHistoryDown: () => dispatch(browseReplHistoryDown(workspaceLocation)), - handleBrowseHistoryUp: () => dispatch(browseReplHistoryUp(workspaceLocation)), - handleClearContext: (library: Library, shouldInitLibrary: boolean) => - dispatch(beginClearContext(workspaceLocation, library, shouldInitLibrary)), - handleDeclarationNavigate: (cursorPosition: Position) => - dispatch(navigateToDeclaration(workspaceLocation, cursorPosition)), - handleEditorEval: () => dispatch(evalEditor(workspaceLocation)), - handleEditorValueChange: (editorTabIndex: number, newEditorValue: string) => - dispatch(updateEditorValue(workspaceLocation, editorTabIndex, newEditorValue)), - handleEditorUpdateBreakpoints: (editorTabIndex: number, newBreakpoints: string[]) => - dispatch(setEditorBreakpoint(workspaceLocation, editorTabIndex, newBreakpoints)), - handleReplEval: () => dispatch(evalRepl(workspaceLocation)), - handleReplOutputClear: () => dispatch(clearReplOutput(workspaceLocation)), - handleReplValueChange: (newValue: string) => - dispatch(updateReplValue(newValue, workspaceLocation)), - handleResetWorkspace: (options: Partial) => - dispatch(resetWorkspace(workspaceLocation, options)), - handleUpdateWorkspace: (options: Partial) => - dispatch(updateWorkspace(workspaceLocation, options)), handleSubmitAnswer: (id: number, answer: string | number) => dispatch(submitAnswer(id, answer)), - handleSideContentHeightChange: (heightChange: number) => - dispatch(changeSideContentHeight(heightChange, workspaceLocation)), - handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) => - dispatch(updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges)), handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => - dispatch(updateCurrentAssessmentId(assessmentId, questionId)), - handlePromptAutocomplete: (row: number, col: number, callback: any) => - dispatch(promptAutocomplete(workspaceLocation, row, col, callback)), - setActiveEditorTabIndex: (activeEditorTabIndex: number | null) => - dispatch(updateActiveEditorTabIndex(workspaceLocation, activeEditorTabIndex)), - removeEditorTabByIndex: (editorTabIndex: number) => - dispatch(removeEditorTab(workspaceLocation, editorTabIndex)) + dispatch(allWorkspaceActions.updateCurrentAssessmentId(workspaceLocation, assessmentId, questionId)), }; }, [dispatch]); @@ -248,7 +198,6 @@ const EditingWorkspace: React.FC = props => { setHasUnsavedChanges(false); setShowResetTemplateOverlay(false); setOriginalMaxXp(getMaxXp()); - handleRefreshLibrary(); resetWorkspaceValues(); }} options={{ minimal: false, intent: Intent.DANGER }} @@ -280,28 +229,27 @@ const EditingWorkspace: React.FC = props => { setAssessment(retrieveLocalAssessment()); setHasUnsavedChanges(false); } - handleRefreshLibrary(); } } - const handleRefreshLibrary = (library: Library | undefined = undefined) => { - const question = assessment!.questions[formatedQuestionId()]; - if (!library) { - library = question.library.chapter === -1 ? assessment!.globalDeployment! : question.library; - } - if (library && library.globals.length > 0) { - const globalsVal = library.globals.map((x: any) => x[0]); - const symbolsVal = library.external.symbols.concat(globalsVal); - library = { - ...library, - external: { - name: library.external.name, - symbols: uniq(symbolsVal) - } - }; - } - handleClearContext(library, true); - }; + // const handleRefreshLibrary = (library: Library | undefined = undefined) => { + // const question = assessment!.questions[formatedQuestionId()]; + // if (!library) { + // library = question.library.chapter === -1 ? assessment!.globalDeployment! : question.library; + // } + // if (library && library.globals.length > 0) { + // const globalsVal = library.globals.map((x: any) => x[0]); + // const symbolsVal = library.external.symbols.concat(globalsVal); + // library = { + // ...library, + // external: { + // name: library.external.name, + // symbols: uniq(symbolsVal) + // } + // }; + // } + // handleClearContext(true); + // }; const resetWorkspaceValues = () => { const question: Question = assessment!.questions[formatedQuestionId()]; @@ -322,13 +270,16 @@ const EditingWorkspace: React.FC = props => { handleResetWorkspace({ // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - editorTabs: [ - { - value: editorValue, - highlightedLines: [], - breakpoints: [] - } - ], + editorState: { + + editorTabs: [ + { + value: editorValue, + highlightedLines: [], + breakpoints: [] + } + ], + }, programPrependValue, programPostpendValue }); @@ -339,7 +290,7 @@ const EditingWorkspace: React.FC = props => { const handleTestcaseEval = (testcase: Testcase) => { const editorTestcases = [testcase]; handleUpdateWorkspace({ editorTestcases }); - dispatch(evalTestcase(workspaceLocation, 0)); + dispatch(allWorkspaceActions.evalTestCase(workspaceLocation, 0)); }; const handleSave = () => { @@ -381,7 +332,6 @@ const EditingWorkspace: React.FC = props => { const updateAndSaveAssessment = (assessmentVal: Assessment) => { setAssessment(assessmentVal); - handleRefreshLibrary(); handleSave(); resetWorkspaceValues(); }; @@ -445,7 +395,6 @@ const EditingWorkspace: React.FC = props => { = props => { = props => { = props => { = props => { } return { - tabs: { beforeDynamicTabs: tabs, afterDynamicTabs: [] } + tabs: { beforeDynamicTabs: tabs, afterDynamicTabs: [] }, + location: workspaceLocation, }; }; @@ -663,10 +610,10 @@ const EditingWorkspace: React.FC = props => { question.type === QuestionTypes.programming ? { editorVariant: 'normal', - isFolderModeEnabled, + editorSessionId, activeEditorTabIndex, - setActiveEditorTabIndex, - removeEditorTabByIndex, + isEditorAutorun, + isFolderModeEnabled, editorTabs: editorTabs .map(convertEditorTabStateToProps) .map((editorTabStateProps, index) => { @@ -683,17 +630,15 @@ const EditingWorkspace: React.FC = props => { (question as IProgrammingQuestion).solutionTemplate }; }), - editorSessionId: '', - handleDeclarationNavigate: handleDeclarationNavigate, - handleEditorEval: handleEditorEval, - handleEditorValueChange: handleEditorValueChange, - handleEditorUpdateBreakpoints: handleEditorUpdateBreakpoints, - handleUpdateHasUnsavedChanges: handleUpdateHasUnsavedChanges, - handlePromptAutocomplete: handlePromptAutocomplete, - isEditorAutorun: false + handleDeclarationNavigate, + handlePromptAutocomplete, + setActiveEditorTabIndex: handleUpdateActiveEditorTabIndex, + removeEditorTabByIndex: handleRemoveEditorTabByIndex, + handleEditorUpdateBreakpoints, + handleEditorEval, + handleEditorValueChange, } : undefined, - handleSideContentHeightChange: handleSideContentHeightChange, hasUnsavedChanges: hasUnsavedChanges, mcqProps: { mcq: question as IMCQQuestion, @@ -703,20 +648,17 @@ const EditingWorkspace: React.FC = props => { sideBarProps: { tabs: [] }, - sideContentHeight: sideContentHeight, sideContentProps: sideContentProps(props, questionId), replProps: { - handleBrowseHistoryDown: handleBrowseHistoryDown, - handleBrowseHistoryUp: handleBrowseHistoryUp, + location: workspaceLocation, handleReplEval: handleReplEval, - handleReplValueChange: handleReplValueChange, output: output, - replValue: replValue, sourceChapter: question?.library?.chapter || Chapter.SOURCE_4, sourceVariant: Variant.DEFAULT, // externalLibrary: question?.library?.external?.name || 'NONE', replButtons: replButtons() - } + }, + workspaceLocation }; return (
@@ -726,9 +668,9 @@ const EditingWorkspace: React.FC = props => { ); }; -function uniq(a: string[]) { - const seen = {}; - return a.filter(item => (seen.hasOwnProperty(item) ? false : (seen[item] = true))); -} +// function uniq(a: string[]) { +// const seen = {}; +// return a.filter(item => (seen.hasOwnProperty(item) ? false : (seen[item] = true))); +// } export default EditingWorkspace; diff --git a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentDeploymentTab.tsx b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentDeploymentTab.tsx index 4485f14789..8c10c6bc97 100644 --- a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentDeploymentTab.tsx +++ b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentDeploymentTab.tsx @@ -5,11 +5,6 @@ import { Chapter, Variant } from 'js-slang/dist/types'; import React from 'react'; import { SALanguage, sourceLanguages, styliseSublanguage } from '../application/ApplicationTypes'; -import { - External, - externalLibraries, - ExternalLibraryName -} from '../application/types/ExternalTypes'; import { Assessment, emptyLibrary, Library } from '../assessment/AssessmentTypes'; import ControlButton from '../ControlButton'; import { assignToPath, getValueFromPath } from './EditingWorkspaceSideContentHelper'; @@ -19,7 +14,7 @@ type DeploymentTabProps = DispatchProps & StateProps; type DispatchProps = { updateAssessment: (assessment: Assessment) => void; - handleRefreshLibrary: (library: Library) => void; + // handleRefreshLibrary: (library: Library) => void; }; type StateProps = { @@ -56,19 +51,19 @@ const DeploymentTab: React.FC = props => { )); - const resetLibrary = ( - props.handleRefreshLibrary(deployment)} - /> - ); + // const resetLibrary = ( + // props.handleRefreshLibrary(deployment)} + // /> + // ); const symbolsFragment = ( External Library:
- {externalSelect(deployment.external.name, handleExternalSelect)} + {/* {externalSelect(deployment.external.name, handleExternalSelect)} */}
Symbols:

@@ -94,8 +89,8 @@ const DeploymentTab: React.FC = props => {
{/* {deploymentDisp}
*/} - - {resetLibrary} + {/* + {resetLibrary} */} Interpreter:
@@ -180,13 +175,12 @@ const DeploymentTab: React.FC = props => { props.updateAssessment(assessment); }; - const handleExternalSelect = (i: External, _e?: React.SyntheticEvent) => { - const assessment = props.assessment; - const deployment = getValueFromPath(props.pathToLibrary, assessment) as Library; - deployment.external.name = i.name; - deployment.external.symbols = JSON.parse(JSON.stringify(externalLibraries.get(i.name)!)); - props.updateAssessment(assessment); - }; + // const handleExternalSelect = (i: External, _e?: React.SyntheticEvent) => { + // const assessment = props.assessment; + // const deployment = getValueFromPath(props.pathToLibrary, assessment) as Library; + // deployment.external.symbols = JSON.parse(JSON.stringify(externalLibraries.get(i.name)!)); + // props.updateAssessment(assessment); + // }; const handleSwitchDeployment = () => { const assessment = props.assessment; @@ -267,35 +261,35 @@ const chapterRenderer: ItemRenderer = (chap, { handleClick, modifier ); -const iExternals = Array.from(externalLibraries.entries()).map((entry, index) => ({ - name: entry[0] as ExternalLibraryName, - key: index, - symbols: entry[1] -})); - -const externalSelect = ( - currentExternal: string, - handleSelect: (i: External, e?: React.SyntheticEvent) => void -) => ( - -