From 68abcadd70d0c9b09729a66cb0d3f0b50e99e980 Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Wed, 7 Feb 2024 21:27:38 +0800 Subject: [PATCH 01/46] Add draft read-only workspace feature --- public/externalLibs/sound/soundToneMatrix.js | 6 ++-- .../AssessmentWorkspace.tsx | 33 ++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/public/externalLibs/sound/soundToneMatrix.js b/public/externalLibs/sound/soundToneMatrix.js index 8638a90378..d0246757bf 100644 --- a/public/externalLibs/sound/soundToneMatrix.js +++ b/public/externalLibs/sound/soundToneMatrix.js @@ -36,7 +36,7 @@ var timeout_matrix; // for coloring the matrix accordingly while it's being played var timeout_color; -var timeout_objects = new Array(); +var timeout_objects = []; // vector_to_list returns a list that contains the elements of the argument vector // in the given order. @@ -54,7 +54,7 @@ function vector_to_list(vector) { function x_y_to_row_column(x, y) { var row = Math.floor((y - margin_length) / (square_side_length + distance_between_squares)); var column = Math.floor((x - margin_length) / (square_side_length + distance_between_squares)); - return Array(row, column); + return [row, column]; } // given the row number of a square, return the leftmost coordinate @@ -365,5 +365,5 @@ function clear_all_timeout() { clearTimeout(timeout_objects[i]); } - timeout_objects = new Array(); + timeout_objects = []; } diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 6aa3fd43c9..f12ef1b4eb 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -109,6 +109,9 @@ const AssessmentWorkspace: React.FC = props => { const [showResetTemplateOverlay, setShowResetTemplateOverlay] = useState(false); const [sessionId, setSessionId] = useState(''); const { isMobileBreakpoint } = useResponsive(); + // isEditable is a placeholder for now. In the future, it should be set to be + // based on whether it is the actual question being attempted. To enable read-only mode, set isEditable to false. + const isEditable = false; const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); const [selectedTab, setSelectedTab] = useState( @@ -247,6 +250,35 @@ const AssessmentWorkspace: React.FC = props => { } }, [isMobileBreakpoint, props, selectedTab]); + useEffect(() => { + const handleReadOnlyMode = (event: KeyboardEvent) => { + // console.log(event.key); + if ( + !event.ctrlKey && + !event.altKey && + !event.metaKey && + event.key !== 'ArrowLeft' && + event.key !== 'ArrowRight' && + event.key !== 'ArrowUp' && + event.key !== 'ArrowDown' // Allow Meta (Command) key combinations for navigation on macOS + ) { + // console.log("Preventing default behavior of key press"); + event.stopPropagation(); + // Prevent default behavior of the key press if they change the editor + event.preventDefault(); + } + }; + + if (!isEditable) { + document.body.addEventListener('keydown', handleReadOnlyMode, true); + } + + // Remove the event listener when the component unmounts + return () => { + document.body.removeEventListener('keydown', handleReadOnlyMode, true); + }; + }); + /* ================== onChange handlers ================== */ @@ -256,7 +288,6 @@ const AssessmentWorkspace: React.FC = props => { handleUpdateHasUnsavedChanges?.(true); // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. handleEditorValueChange(0, newCode); - const input: Input = { time: Date.now(), type: 'codeDelta', From bfcc2a68e4fbc1ad9e8d8aed708660eddbf9cab3 Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Wed, 7 Feb 2024 23:08:03 +0800 Subject: [PATCH 02/46] Change isEditable to true --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index f12ef1b4eb..c382ac401c 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -111,7 +111,7 @@ const AssessmentWorkspace: React.FC = props => { const { isMobileBreakpoint } = useResponsive(); // isEditable is a placeholder for now. In the future, it should be set to be // based on whether it is the actual question being attempted. To enable read-only mode, set isEditable to false. - const isEditable = false; + const isEditable = true; const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); const [selectedTab, setSelectedTab] = useState( From a5395e5dfaf3b2a363870c38347ec2c699d47aca Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 9 Feb 2024 00:40:55 +0800 Subject: [PATCH 03/46] Add file view to assessments --- .../AssessmentWorkspace.tsx | 167 +++++++++++------- src/commons/fileSystemView/FileSystemView.tsx | 9 +- .../FileSystemViewContextMenu.tsx | 8 +- .../fileSystem/createInBrowserFileSystem.ts | 8 +- 4 files changed, 123 insertions(+), 69 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index c382ac401c..8069f2d585 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -17,6 +17,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router'; import { onClickProgress } from 'src/features/assessments/AssessmentUtils'; +import { WORKSPACE_BASE_PATHS } from 'src/pages/fileSystem/createInBrowserFileSystem'; import { mobileOnlyTabIds } from 'src/pages/playground/PlaygroundTabs'; import { initSession, log } from '../../features/eventLogging'; @@ -49,12 +50,15 @@ import { ControlBarQuestionViewButton } from '../controlBar/ControlBarQuestionVi import { ControlBarResetButton } from '../controlBar/ControlBarResetButton'; import { ControlBarRunButton } from '../controlBar/ControlBarRunButton'; import { ControlButtonSaveButton } from '../controlBar/ControlBarSaveButton'; +import { ControlBarToggleFolderModeButton } from '../controlBar/ControlBarToggleFolderModeButton'; import ControlButton from '../ControlButton'; import { convertEditorTabStateToProps, NormalEditorContainerProps } from '../editor/EditorContainer'; import { Position } from '../editor/EditorTypes'; +import { overwriteFilesInWorkspace } from '../fileSystem/utils'; +import FileSystemView from '../fileSystemView/FileSystemView'; import Markdown from '../Markdown'; import { MobileSideContentProps } from '../mobileWorkspace/mobileSideContent/MobileSideContent'; import MobileWorkspace, { MobileWorkspaceProps } from '../mobileWorkspace/MobileWorkspace'; @@ -70,6 +74,7 @@ import { assessmentTypeLink } from '../utils/ParamParseHelper'; import { assertType } from '../utils/TypeHelper'; import Workspace, { WorkspaceProps } from '../workspace/Workspace'; import { + addEditorTab, beginClearContext, browseReplHistoryDown, browseReplHistoryUp, @@ -85,6 +90,7 @@ import { resetWorkspace, runAllTestcases, setEditorBreakpoint, + toggleFolderMode, updateActiveEditorTabIndex, updateCurrentAssessmentId, updateEditorValue, @@ -121,6 +127,7 @@ const AssessmentWorkspace: React.FC = props => { ); const navigate = useNavigate(); + const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); const { courseId } = useTypedSelector(state => state.session); const { @@ -180,12 +187,6 @@ const AssessmentWorkspace: React.FC = props => { }; }, [dispatch]); - useEffect(() => { - // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. - handleEditorValueChange(0, ''); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - /** * After mounting (either an older copy of the assessment * or a loading screen), try to fetch a newer assessment, @@ -210,28 +211,42 @@ const AssessmentWorkspace: React.FC = props => { if (!assessment) { return; } - // ------------- PLEASE NOTE, EVERYTHING BELOW THIS SEEMS TO BE UNUSED ------------- - // checkWorkspaceReset does exactly the same thing. - let questionId = props.questionId; - if (props.questionId >= assessment.questions.length) { - questionId = assessment.questions.length - 1; - } + // Remove the event listener when the component unmounts + return () => { + document.body.removeEventListener('keydown', handleReadOnlyMode, true); + }; - const question = assessment.questions[questionId]; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - let answer = ''; - if (question.type === QuestionTypes.programming) { - if (question.answer) { - answer = (question as IProgrammingQuestion).answer as string; - } else { - answer = (question as IProgrammingQuestion).solutionTemplate; - } + const handleReadOnlyMode = (event: KeyboardEvent) => { + // console.log(event.key); + if ( + !event.ctrlKey && + !event.altKey && + !event.metaKey && + event.key !== 'ArrowLeft' && + event.key !== 'ArrowRight' && + event.key !== 'ArrowUp' && + event.key !== 'ArrowDown' // Allow Meta (Command) key combinations for navigation on macOS + ) { + // console.log("Preventing default behavior of key press"); + event.stopPropagation(); + // Prevent default behavior of the key press if they change the editor + event.preventDefault(); } + }; + + useEffect(() => { + if (!isEditable) { + document.body.addEventListener('keydown', handleReadOnlyMode, true); + } + else { + document.body.removeEventListener('keydown', handleReadOnlyMode, true); + } + }, [isEditable]) - // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. - handleEditorValueChange(0, answer); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); /** * Once there is an update (due to the assessment being fetched), check @@ -250,44 +265,21 @@ const AssessmentWorkspace: React.FC = props => { } }, [isMobileBreakpoint, props, selectedTab]); - useEffect(() => { - const handleReadOnlyMode = (event: KeyboardEvent) => { - // console.log(event.key); - if ( - !event.ctrlKey && - !event.altKey && - !event.metaKey && - event.key !== 'ArrowLeft' && - event.key !== 'ArrowRight' && - event.key !== 'ArrowUp' && - event.key !== 'ArrowDown' // Allow Meta (Command) key combinations for navigation on macOS - ) { - // console.log("Preventing default behavior of key press"); - event.stopPropagation(); - // Prevent default behavior of the key press if they change the editor - event.preventDefault(); - } - }; - - if (!isEditable) { - document.body.addEventListener('keydown', handleReadOnlyMode, true); - } - - // Remove the event listener when the component unmounts - return () => { - document.body.removeEventListener('keydown', handleReadOnlyMode, true); - }; - }); - /* ================== onChange handlers ================== */ const pushLog = useCallback((newInput: Input) => log(sessionId, newInput), [sessionId]); + const onEditorValueChange = React.useCallback( + (editorTabIndex: number, newEditorValue: string) => { + handleEditorValueChange(editorTabIndex, newEditorValue); + }, + [handleEditorValueChange] + ); + const onChangeMethod = (newCode: string, delta: CodeDelta) => { handleUpdateHasUnsavedChanges?.(true); // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. - handleEditorValueChange(0, newCode); const input: Input = { time: Date.now(), type: 'codeDelta', @@ -342,6 +334,16 @@ const AssessmentWorkspace: React.FC = props => { pushLog(input); }, [handleEditorEval, handleRunAllTestcases, pushLog]); + // Rewrites the file system with our desired file tree + const rewriteFilesWithContent = async (currentQuestionFilePath: string, newFileTree: Record) => { + if (fileSystem) { + await overwriteFilesInWorkspace(workspaceLocation, fileSystem, newFileTree); + dispatch(removeEditorTab(workspaceLocation, 0)) // remove the default tab which keeps appearing ;c + dispatch(addEditorTab(workspaceLocation, currentQuestionFilePath, newFileTree[currentQuestionFilePath] ?? "")) + dispatch(updateActiveEditorTabIndex(workspaceLocation, 0)); + } + } + /* ================ Helper Functions ================ */ @@ -379,16 +381,24 @@ const AssessmentWorkspace: React.FC = props => { options.programPostpendValue = programmingQuestionData.postpend; options.editorTestcases = programmingQuestionData.testcases; + // We use || not ?? to match both null and an empty string - options.editorValue = - programmingQuestionData.answer || programmingQuestionData.solutionTemplate; + // Sets the current active tab to the current "question file" and also force re-writes the file system + const currentQuestionFilePath = `${workspaceLocation}/${questionId+1}.js` + rewriteFilesWithContent( + currentQuestionFilePath, + { + [currentQuestionFilePath]: programmingQuestionData.answer || programmingQuestionData.solutionTemplate + }) + // Initialize session once the editorValue is known. + if (!sessionId) { setSessionId( initSession(`${(assessment as any).number}/${props.questionId}`, { chapter: question.library.chapter, externalLibrary: question?.library?.external?.name || 'NONE', - editorValue: options.editorValue + editorValue: programmingQuestionData.answer || programmingQuestionData.solutionTemplate }) ); } @@ -422,7 +432,7 @@ const AssessmentWorkspace: React.FC = props => { 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. - handleEditorValueChange(0, options.editorValue); + //handleEditorValueChange(0, options.editorValue); } }; @@ -439,7 +449,7 @@ const AssessmentWorkspace: React.FC = props => { const isContestVoting = question?.type === QuestionTypes.voting; const handleContestEntryClick = (_submissionId: number, answer: string) => { // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. - handleEditorValueChange(0, answer); + //handleEditorValueChange(0, answer); }; const tabs: SideContentTab[] = [ @@ -660,9 +670,19 @@ const AssessmentWorkspace: React.FC = props => { /> ); + const toggleFolderModeButton = ( + dispatch(toggleFolderMode(workspaceLocation))} + key="folder" + /> + ); + return { editorButtons: !isMobileBreakpoint - ? [runButton, saveButton, resetButton, chapterSelect] + ? [runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] : [saveButton, resetButton], flowButtons: [previousButton, questionView, nextButton] }; @@ -788,10 +808,10 @@ const AssessmentWorkspace: React.FC = props => { onClick={() => { closeOverlay(); // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. - handleEditorValueChange( + /*handleEditorValueChange( 0, (assessment!.questions[questionId] as IProgrammingQuestion).solutionTemplate - ); + );*/ handleUpdateHasUnsavedChanges(true); }} options={{ minimal: false, intent: Intent.DANGER }} @@ -822,7 +842,7 @@ const AssessmentWorkspace: React.FC = props => { externalLibraryName: question.library.external.name || 'NONE', handleDeclarationNavigate: editorContainerHandlers.handleDeclarationNavigate, handleEditorEval: handleEval, - handleEditorValueChange: handleEditorValueChange, + handleEditorValueChange: onEditorValueChange, handleUpdateHasUnsavedChanges: handleUpdateHasUnsavedChanges, handleEditorUpdateBreakpoints: handleEditorUpdateBreakpoints, handlePromptAutocomplete: editorContainerHandlers.handlePromptAutocomplete, @@ -849,7 +869,25 @@ const AssessmentWorkspace: React.FC = props => { replButtons: replButtons }; const sideBarProps = { - tabs: [] + tabs: [ + ...(isFolderModeEnabled + ? [ + { + label: 'Folder', + body: ( + + ), + iconName: IconNames.FOLDER_CLOSE, + id: SideContentType.folder + } + ] + : []) + ] }; const workspaceProps: WorkspaceProps = { controlBarProps: controlBarProps(questionId), @@ -870,6 +908,7 @@ const AssessmentWorkspace: React.FC = props => { sideBarProps: sideBarProps, mobileSideContentProps: mobileSideContentProps(questionId) }; + console.log(workspaceProps.editorContainerProps?.editorTabs) return (
diff --git a/src/commons/fileSystemView/FileSystemView.tsx b/src/commons/fileSystemView/FileSystemView.tsx index 5aeb2b6590..3dbbdbd639 100644 --- a/src/commons/fileSystemView/FileSystemView.tsx +++ b/src/commons/fileSystemView/FileSystemView.tsx @@ -15,10 +15,12 @@ import FileSystemViewPlaceholderNode from './FileSystemViewPlaceholderNode'; export type FileSystemViewProps = { workspaceLocation: WorkspaceLocation; basePath: string; + disableNewFile?: boolean; // Disables creation of new files + disableNewFolder?: boolean; // Disables creation of new folders }; const FileSystemView: React.FC = (props: FileSystemViewProps) => { - const { workspaceLocation, basePath } = props; + const { workspaceLocation, basePath, disableNewFile, disableNewFolder } = props; const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); const [isAddingNewFile, setIsAddingNewFile] = React.useState(false); @@ -122,11 +124,16 @@ const FileSystemView: React.FC = (props: FileSystemViewProp />
)} + + {(!disableNewFile || !disableNewFolder) && ( + )} ); }; diff --git a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx index 51c8a68dee..a14e97b8ef 100644 --- a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx +++ b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx @@ -7,6 +7,8 @@ import classes from 'src/styles/ContextMenu.module.scss'; export type FileSystemViewContextMenuProps = { children?: JSX.Element; className?: string; + disableNewFile?: boolean; + disableNewFolder?: boolean; createNewFile?: () => void; createNewDirectory?: () => void; open?: () => void; @@ -17,7 +19,7 @@ export type FileSystemViewContextMenuProps = { const FileSystemViewContextMenu: React.FC = ( props: FileSystemViewContextMenuProps ) => { - const { children, className, createNewFile, createNewDirectory, open, rename, remove } = props; + const { children, className, createNewFile, createNewDirectory, open, rename, remove, disableNewFile, disableNewFolder } = props; const [menuProps, toggleMenu] = useMenuState(); const [anchorPoint, setAnchorPoint] = React.useState({ x: 0, y: 0 }); @@ -36,12 +38,12 @@ const FileSystemViewContextMenu: React.FC = ( anchorPoint={anchorPoint} onClose={() => toggleMenu(false)} > - {createNewFile && ( + {createNewFile && !disableNewFile && ( New File )} - {createNewDirectory && ( + {createNewDirectory && !disableNewFolder && ( New Directory diff --git a/src/pages/fileSystem/createInBrowserFileSystem.ts b/src/pages/fileSystem/createInBrowserFileSystem.ts index 86ab66b937..ab9f294180 100644 --- a/src/pages/fileSystem/createInBrowserFileSystem.ts +++ b/src/pages/fileSystem/createInBrowserFileSystem.ts @@ -14,7 +14,7 @@ import { EditorTabState, WorkspaceManagerState } from '../../commons/workspace/W * linked to the file system. */ export const WORKSPACE_BASE_PATHS: Record = { - assessment: '', + assessment: '/assessment', githubAssessment: '', grading: '', playground: '/playground', @@ -41,6 +41,12 @@ export const createInBrowserFileSystem = (store: Store): Promise Date: Tue, 13 Feb 2024 12:34:27 +0800 Subject: [PATCH 04/46] Move enable read-only workspace to onEditorValueChange --- .../AssessmentWorkspace.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 8069f2d585..b8d54df374 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -210,16 +210,11 @@ const AssessmentWorkspace: React.FC = props => { } if (!assessment) { return; - } - // Remove the event listener when the component unmounts - return () => { - document.body.removeEventListener('keydown', handleReadOnlyMode, true); - }; - - + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + /* const handleReadOnlyMode = (event: KeyboardEvent) => { // console.log(event.key); if ( @@ -246,7 +241,7 @@ const AssessmentWorkspace: React.FC = props => { document.body.removeEventListener('keydown', handleReadOnlyMode, true); } }, [isEditable]) - + */ /** * Once there is an update (due to the assessment being fetched), check @@ -272,9 +267,11 @@ const AssessmentWorkspace: React.FC = props => { const onEditorValueChange = React.useCallback( (editorTabIndex: number, newEditorValue: string) => { - handleEditorValueChange(editorTabIndex, newEditorValue); - }, - [handleEditorValueChange] + if (isEditable) { + handleEditorValueChange(editorTabIndex, newEditorValue); + } + }, + [handleEditorValueChange, isEditable] ); const onChangeMethod = (newCode: string, delta: CodeDelta) => { From 4e6d2cf945763ac71d313727fe3eb474eba156c5 Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Tue, 13 Feb 2024 13:07:10 +0800 Subject: [PATCH 05/46] Change control bar for read only mode --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index b8d54df374..f43e92d7a1 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -676,12 +676,16 @@ const AssessmentWorkspace: React.FC = props => { key="folder" /> ); - + + const editorButtonsMobileBreakpoint = isEditable ? [runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] + : [toggleFolderModeButton, chapterSelect]; + const editorButtonsNotMobileBreakpoint = isEditable ? [saveButton, resetButton] : []; + const flowButtons = isEditable ? [previousButton, questionView, nextButton] : [questionView]; return { editorButtons: !isMobileBreakpoint - ? [runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] - : [saveButton, resetButton], - flowButtons: [previousButton, questionView, nextButton] + ? editorButtonsMobileBreakpoint + : editorButtonsNotMobileBreakpoint, + flowButtons: flowButtons }; }; From f41080339f39aab4471dff33394c5bb1400e99ee Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Tue, 13 Feb 2024 13:47:05 +0800 Subject: [PATCH 06/46] Add file mode control bar button --- .../AssessmentWorkspace.tsx | 72 +++++++++++-------- .../controlBar/ControlBarFileModeButton.tsx | 19 +++++ src/commons/fileSystemView/FileSystemView.tsx | 18 ++--- .../FileSystemViewContextMenu.tsx | 12 +++- 4 files changed, 81 insertions(+), 40 deletions(-) create mode 100644 src/commons/controlBar/ControlBarFileModeButton.tsx diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index f43e92d7a1..bd15eb2a6f 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -44,6 +44,7 @@ import { ControlBarProps } from '../controlBar/ControlBar'; import { ControlBarChapterSelect } from '../controlBar/ControlBarChapterSelect'; import { ControlBarClearButton } from '../controlBar/ControlBarClearButton'; import { ControlBarEvalButton } from '../controlBar/ControlBarEvalButton'; +import { ControlBarFileModeButton } from '../controlBar/ControlBarFileModeButton'; import { ControlBarNextButton } from '../controlBar/ControlBarNextButton'; import { ControlBarPreviousButton } from '../controlBar/ControlBarPreviousButton'; import { ControlBarQuestionViewButton } from '../controlBar/ControlBarQuestionViewButton'; @@ -210,7 +211,7 @@ const AssessmentWorkspace: React.FC = props => { } if (!assessment) { return; - } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -270,7 +271,7 @@ const AssessmentWorkspace: React.FC = props => { if (isEditable) { handleEditorValueChange(editorTabIndex, newEditorValue); } - }, + }, [handleEditorValueChange, isEditable] ); @@ -332,14 +333,23 @@ const AssessmentWorkspace: React.FC = props => { }, [handleEditorEval, handleRunAllTestcases, pushLog]); // Rewrites the file system with our desired file tree - const rewriteFilesWithContent = async (currentQuestionFilePath: string, newFileTree: Record) => { + const rewriteFilesWithContent = async ( + currentQuestionFilePath: string, + newFileTree: Record + ) => { if (fileSystem) { await overwriteFilesInWorkspace(workspaceLocation, fileSystem, newFileTree); - dispatch(removeEditorTab(workspaceLocation, 0)) // remove the default tab which keeps appearing ;c - dispatch(addEditorTab(workspaceLocation, currentQuestionFilePath, newFileTree[currentQuestionFilePath] ?? "")) + dispatch(removeEditorTab(workspaceLocation, 0)); // remove the default tab which keeps appearing ;c + dispatch( + addEditorTab( + workspaceLocation, + currentQuestionFilePath, + newFileTree[currentQuestionFilePath] ?? '' + ) + ); dispatch(updateActiveEditorTabIndex(workspaceLocation, 0)); } - } + }; /* ================ Helper Functions @@ -378,24 +388,23 @@ const AssessmentWorkspace: React.FC = props => { options.programPostpendValue = programmingQuestionData.postpend; options.editorTestcases = programmingQuestionData.testcases; - // We use || not ?? to match both null and an empty string // Sets the current active tab to the current "question file" and also force re-writes the file system - const currentQuestionFilePath = `${workspaceLocation}/${questionId+1}.js` - rewriteFilesWithContent( - currentQuestionFilePath, - { - [currentQuestionFilePath]: programmingQuestionData.answer || programmingQuestionData.solutionTemplate - }) - + const currentQuestionFilePath = `${workspaceLocation}/${questionId + 1}.js`; + rewriteFilesWithContent(currentQuestionFilePath, { + [currentQuestionFilePath]: + programmingQuestionData.answer || programmingQuestionData.solutionTemplate + }); + // Initialize session once the editorValue is known. - + if (!sessionId) { setSessionId( initSession(`${(assessment as any).number}/${props.questionId}`, { chapter: question.library.chapter, externalLibrary: question?.library?.external?.name || 'NONE', - editorValue: programmingQuestionData.answer || programmingQuestionData.solutionTemplate + editorValue: + programmingQuestionData.answer || programmingQuestionData.solutionTemplate }) ); } @@ -668,17 +677,20 @@ const AssessmentWorkspace: React.FC = props => { ); const toggleFolderModeButton = ( - dispatch(toggleFolderMode(workspaceLocation))} - key="folder" - /> - ); - - const editorButtonsMobileBreakpoint = isEditable ? [runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] - : [toggleFolderModeButton, chapterSelect]; + dispatch(toggleFolderMode(workspaceLocation))} + key="folder" + /> + ); + + const fileModeButton = ; + + const editorButtonsMobileBreakpoint = isEditable + ? [fileModeButton, runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] + : [fileModeButton, toggleFolderModeButton, chapterSelect]; const editorButtonsNotMobileBreakpoint = isEditable ? [saveButton, resetButton] : []; const flowButtons = isEditable ? [previousButton, questionView, nextButton] : [questionView]; return { @@ -877,8 +889,8 @@ const AssessmentWorkspace: React.FC = props => { label: 'Folder', body: ( @@ -909,7 +921,7 @@ const AssessmentWorkspace: React.FC = props => { sideBarProps: sideBarProps, mobileSideContentProps: mobileSideContentProps(questionId) }; - console.log(workspaceProps.editorContainerProps?.editorTabs) + console.log(workspaceProps.editorContainerProps?.editorTabs); return (
diff --git a/src/commons/controlBar/ControlBarFileModeButton.tsx b/src/commons/controlBar/ControlBarFileModeButton.tsx new file mode 100644 index 0000000000..4fe32aa27e --- /dev/null +++ b/src/commons/controlBar/ControlBarFileModeButton.tsx @@ -0,0 +1,19 @@ +import { IconNames } from '@blueprintjs/icons'; +import React from 'react'; + +import ControlButton from '../ControlButton'; + +/** + * @prop fileMode an integer for the mode of the file where + * 0 = read-only and 1 = read-write. + */ +type ControlBarFileModeButtonProps = { + fileMode: number | null; +}; + +export const ControlBarFileModeButton: React.FC = ({ fileMode }) => { + if (fileMode === 0) { + return ; + } + return ; +}; diff --git a/src/commons/fileSystemView/FileSystemView.tsx b/src/commons/fileSystemView/FileSystemView.tsx index 3dbbdbd639..cc1e85a1ea 100644 --- a/src/commons/fileSystemView/FileSystemView.tsx +++ b/src/commons/fileSystemView/FileSystemView.tsx @@ -124,16 +124,16 @@ const FileSystemView: React.FC = (props: FileSystemViewProp />
)} - + {(!disableNewFile || !disableNewFolder) && ( - - )} + + )} ); }; diff --git a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx index a14e97b8ef..8eabef22c6 100644 --- a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx +++ b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx @@ -19,7 +19,17 @@ export type FileSystemViewContextMenuProps = { const FileSystemViewContextMenu: React.FC = ( props: FileSystemViewContextMenuProps ) => { - const { children, className, createNewFile, createNewDirectory, open, rename, remove, disableNewFile, disableNewFolder } = props; + const { + children, + className, + createNewFile, + createNewDirectory, + open, + rename, + remove, + disableNewFile, + disableNewFolder + } = props; const [menuProps, toggleMenu] = useMenuState(); const [anchorPoint, setAnchorPoint] = React.useState({ x: 0, y: 0 }); From 5a7275997aba94bbf8b54879e458025e59ca5f79 Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Tue, 13 Feb 2024 14:07:52 +0800 Subject: [PATCH 07/46] Ignore read-only unsaved changes --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index bd15eb2a6f..d8fdf9fb23 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -118,7 +118,7 @@ const AssessmentWorkspace: React.FC = props => { const { isMobileBreakpoint } = useResponsive(); // isEditable is a placeholder for now. In the future, it should be set to be // based on whether it is the actual question being attempted. To enable read-only mode, set isEditable to false. - const isEditable = true; + const isEditable = false; const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); const [selectedTab, setSelectedTab] = useState( @@ -276,13 +276,14 @@ const AssessmentWorkspace: React.FC = props => { ); const onChangeMethod = (newCode: string, delta: CodeDelta) => { - handleUpdateHasUnsavedChanges?.(true); + isEditable ? handleUpdateHasUnsavedChanges?.(true) : handleUpdateHasUnsavedChanges?.(false); // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. const input: Input = { time: Date.now(), type: 'codeDelta', data: delta }; + console.log(input); pushLog(input); }; From b4aa8aa244957c6bff4d961eefe09cf7070d47f1 Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Tue, 13 Feb 2024 14:34:41 +0800 Subject: [PATCH 08/46] Readd previous,next question button in read-only --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index d8fdf9fb23..3f6b59c7b7 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -118,7 +118,7 @@ const AssessmentWorkspace: React.FC = props => { const { isMobileBreakpoint } = useResponsive(); // isEditable is a placeholder for now. In the future, it should be set to be // based on whether it is the actual question being attempted. To enable read-only mode, set isEditable to false. - const isEditable = false; + const isEditable = true; const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); const [selectedTab, setSelectedTab] = useState( @@ -693,7 +693,7 @@ const AssessmentWorkspace: React.FC = props => { ? [fileModeButton, runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] : [fileModeButton, toggleFolderModeButton, chapterSelect]; const editorButtonsNotMobileBreakpoint = isEditable ? [saveButton, resetButton] : []; - const flowButtons = isEditable ? [previousButton, questionView, nextButton] : [questionView]; + const flowButtons = [previousButton, questionView, nextButton]; return { editorButtons: !isMobileBreakpoint ? editorButtonsMobileBreakpoint From 91ac95e9005612393b5ce6ab5376c67313b26875 Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Tue, 13 Feb 2024 15:24:44 +0800 Subject: [PATCH 09/46] Add key to fileModeButton in AssessmentWorkspace --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 3f6b59c7b7..9ab988bb41 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -687,7 +687,9 @@ const AssessmentWorkspace: React.FC = props => { /> ); - const fileModeButton = ; + const fileModeButton = ( + + ); const editorButtonsMobileBreakpoint = isEditable ? [fileModeButton, runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] From 040cbe158a1fbec2caf8c163efb3fce6b31ccb3d Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Thu, 15 Feb 2024 22:11:43 +0800 Subject: [PATCH 10/46] Disable renaming/deleting of files --- .../AssessmentWorkspace.tsx | 4 +- src/commons/fileSystemView/FileSystemView.tsx | 11 ++-- .../FileSystemViewContextMenu.tsx | 57 +++++++++---------- .../FileSystemViewDirectoryNode.tsx | 5 +- .../fileSystemView/FileSystemViewFileNode.tsx | 4 +- .../fileSystemView/FileSystemViewList.tsx | 5 +- 6 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 9ab988bb41..3e5cbcb539 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -892,8 +892,7 @@ const AssessmentWorkspace: React.FC = props => { label: 'Folder', body: ( @@ -924,7 +923,6 @@ const AssessmentWorkspace: React.FC = props => { sideBarProps: sideBarProps, mobileSideContentProps: mobileSideContentProps(questionId) }; - console.log(workspaceProps.editorContainerProps?.editorTabs); return (
diff --git a/src/commons/fileSystemView/FileSystemView.tsx b/src/commons/fileSystemView/FileSystemView.tsx index cc1e85a1ea..4fed86f0c9 100644 --- a/src/commons/fileSystemView/FileSystemView.tsx +++ b/src/commons/fileSystemView/FileSystemView.tsx @@ -15,12 +15,11 @@ import FileSystemViewPlaceholderNode from './FileSystemViewPlaceholderNode'; export type FileSystemViewProps = { workspaceLocation: WorkspaceLocation; basePath: string; - disableNewFile?: boolean; // Disables creation of new files - disableNewFolder?: boolean; // Disables creation of new folders + disableEditing?: boolean; // Disables creation/renaming/deleting of files }; const FileSystemView: React.FC = (props: FileSystemViewProps) => { - const { workspaceLocation, basePath, disableNewFile, disableNewFolder } = props; + const { workspaceLocation, basePath, disableEditing } = props; const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); const [isAddingNewFile, setIsAddingNewFile] = React.useState(false); @@ -98,6 +97,7 @@ const FileSystemView: React.FC = (props: FileSystemViewProp return (
= (props: FileSystemViewProp
)} - {(!disableNewFile || !disableNewFolder) && ( - )}
); }; diff --git a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx index 8eabef22c6..f089aec71c 100644 --- a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx +++ b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx @@ -7,13 +7,13 @@ import classes from 'src/styles/ContextMenu.module.scss'; export type FileSystemViewContextMenuProps = { children?: JSX.Element; className?: string; - disableNewFile?: boolean; - disableNewFolder?: boolean; + createNewFile?: () => void; createNewDirectory?: () => void; open?: () => void; rename?: () => void; remove?: () => void; + disableEditing?: boolean; }; const FileSystemViewContextMenu: React.FC = ( @@ -27,8 +27,7 @@ const FileSystemViewContextMenu: React.FC = ( open, rename, remove, - disableNewFile, - disableNewFolder + disableEditing } = props; const [menuProps, toggleMenu] = useMenuState(); const [anchorPoint, setAnchorPoint] = React.useState({ x: 0, y: 0 }); @@ -42,38 +41,36 @@ const FileSystemViewContextMenu: React.FC = ( return (
{children} - toggleMenu(false)} - > - {createNewFile && !disableNewFile && ( + {!disableEditing && ( + toggleMenu(false)} + > New File - )} - {createNewDirectory && !disableNewFolder && ( New Directory - )} - {open && ( - - Open - - )} - {rename && ( - - Rename - - )} - {remove && ( - - Delete - - )} - + {open && ( + + Open + + )} + {rename && ( + + Rename + + )} + {remove && ( + + Delete + + )} + + )}
); }; diff --git a/src/commons/fileSystemView/FileSystemViewDirectoryNode.tsx b/src/commons/fileSystemView/FileSystemViewDirectoryNode.tsx index 4c136de3b1..473ee553b5 100644 --- a/src/commons/fileSystemView/FileSystemViewDirectoryNode.tsx +++ b/src/commons/fileSystemView/FileSystemViewDirectoryNode.tsx @@ -23,6 +23,7 @@ export type FileSystemViewDirectoryNodeProps = { directoryName: string; indentationLevel: number; refreshParentDirectory: () => void; + disableEditing?: boolean; }; const FileSystemViewDirectoryNode: React.FC = ( @@ -34,7 +35,8 @@ const FileSystemViewDirectoryNode: React.FC = basePath, directoryName, indentationLevel, - refreshParentDirectory + refreshParentDirectory, + disableEditing } = props; const fullPath = path.join(basePath, directoryName); @@ -154,6 +156,7 @@ const FileSystemViewDirectoryNode: React.FC = createNewDirectory={handleCreateNewDirectory} rename={handleRenameDirectory} remove={handleRemoveDirectory} + disableEditing={disableEditing} >
diff --git a/src/commons/fileSystemView/FileSystemViewFileNode.tsx b/src/commons/fileSystemView/FileSystemViewFileNode.tsx index 489a76e778..6b894e29fc 100644 --- a/src/commons/fileSystemView/FileSystemViewFileNode.tsx +++ b/src/commons/fileSystemView/FileSystemViewFileNode.tsx @@ -19,13 +19,14 @@ export type FileSystemViewFileNodeProps = { basePath: string; fileName: string; indentationLevel: number; + disableEditing?: boolean; refreshDirectory: () => void; }; const FileSystemViewFileNode: React.FC = ( props: FileSystemViewFileNodeProps ) => { - const { workspaceLocation, fileSystem, basePath, fileName, indentationLevel, refreshDirectory } = + const { workspaceLocation, fileSystem, basePath, fileName, indentationLevel, refreshDirectory, disableEditing } = props; const [isEditing, setIsEditing] = React.useState(false); @@ -90,6 +91,7 @@ const FileSystemViewFileNode: React.FC = ( open={handleOpenFile} rename={handleRenameFile} remove={handleRemoveFile} + disableEditing={disableEditing} >
diff --git a/src/commons/fileSystemView/FileSystemViewList.tsx b/src/commons/fileSystemView/FileSystemViewList.tsx index 1b2ed2daa4..3f7fefedd1 100644 --- a/src/commons/fileSystemView/FileSystemViewList.tsx +++ b/src/commons/fileSystemView/FileSystemViewList.tsx @@ -14,10 +14,11 @@ export type FileSystemViewListProps = { fileSystem: FSModule; basePath: string; indentationLevel: number; + disableEditing?: boolean; }; const FileSystemViewList: React.FC = (props: FileSystemViewListProps) => { - const { workspaceLocation, fileSystem, basePath, indentationLevel } = props; + const { workspaceLocation, fileSystem, basePath, indentationLevel, disableEditing } = props; const [dirNames, setDirNames] = React.useState(undefined); const [fileNames, setFileNames] = React.useState(undefined); @@ -77,6 +78,7 @@ const FileSystemViewList: React.FC = (props: FileSystem {dirNames.map(dirName => { return ( = (props: FileSystem {fileNames.map(fileName => { return ( Date: Thu, 15 Feb 2024 22:28:30 +0800 Subject: [PATCH 11/46] Fix clicking on files causing crashes --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 3 ++- src/commons/fileSystemView/FileSystemView.tsx | 12 ++++++------ .../fileSystemView/FileSystemViewFileNode.tsx | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 3e5cbcb539..39b1c34c22 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -391,7 +391,8 @@ const AssessmentWorkspace: React.FC = props => { // We use || not ?? to match both null and an empty string // Sets the current active tab to the current "question file" and also force re-writes the file system - const currentQuestionFilePath = `${workspaceLocation}/${questionId + 1}.js`; + // The leading slash "/" at the front is VERY IMPORTANT! DO NOT DELETE + const currentQuestionFilePath = `/${workspaceLocation}/${questionId + 1}.js`; rewriteFilesWithContent(currentQuestionFilePath, { [currentQuestionFilePath]: programmingQuestionData.answer || programmingQuestionData.solutionTemplate diff --git a/src/commons/fileSystemView/FileSystemView.tsx b/src/commons/fileSystemView/FileSystemView.tsx index 4fed86f0c9..8160ee829e 100644 --- a/src/commons/fileSystemView/FileSystemView.tsx +++ b/src/commons/fileSystemView/FileSystemView.tsx @@ -97,7 +97,7 @@ const FileSystemView: React.FC = (props: FileSystemViewProp return (
= (props: FileSystemViewProp
)} - + className={classes['file-system-view-empty-space']} + createNewFile={handleCreateNewFile} + createNewDirectory={handleCreateNewDirectory} + />
); }; diff --git a/src/commons/fileSystemView/FileSystemViewFileNode.tsx b/src/commons/fileSystemView/FileSystemViewFileNode.tsx index 6b894e29fc..2af475714d 100644 --- a/src/commons/fileSystemView/FileSystemViewFileNode.tsx +++ b/src/commons/fileSystemView/FileSystemViewFileNode.tsx @@ -36,6 +36,7 @@ const FileSystemViewFileNode: React.FC = ( const handleOpenFile = () => { fileSystem.readFile(fullPath, 'utf-8', (err, fileContents) => { + console.log(fileContents) if (err) { console.error(err); } From b77ebb25d95e5de2407260d4bd7adeb13defd53e Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Thu, 15 Feb 2024 23:20:39 +0800 Subject: [PATCH 12/46] Disable saving etc when not viewing question file --- .../AssessmentWorkspace.tsx | 69 ++++++++----------- .../fileSystemView/FileSystemViewFileNode.tsx | 1 - 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 39b1c34c22..7cdea5a498 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -12,7 +12,7 @@ import { import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; -import { isEqual } from 'lodash'; +import { isEqual, isNull } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router'; @@ -112,23 +112,25 @@ export type AssessmentWorkspaceProps = { const workspaceLocation: WorkspaceLocation = 'assessment'; const AssessmentWorkspace: React.FC = props => { + const { assessmentId, questionId } = props; const [showOverlay, setShowOverlay] = useState(false); const [showResetTemplateOverlay, setShowResetTemplateOverlay] = useState(false); const [sessionId, setSessionId] = useState(''); const { isMobileBreakpoint } = useResponsive(); // isEditable is a placeholder for now. In the future, it should be set to be // based on whether it is the actual question being attempted. To enable read-only mode, set isEditable to false. - const isEditable = true; + const [isEditable, setIsEditable] = useState(true); const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); const [selectedTab, setSelectedTab] = useState( - assessment?.questions[props.questionId].grader !== undefined + assessment?.questions[questionId].grader !== undefined ? SideContentType.grading : SideContentType.questionOverview ); const navigate = useNavigate(); const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); + const { courseId } = useTypedSelector(state => state.session); const { @@ -206,7 +208,7 @@ const AssessmentWorkspace: React.FC = props => { } handleAssessmentFetch(props.assessmentId, assessmentPassword || undefined); - if (props.questionId === 0 && props.notAttempted) { + if (questionId === 0 && props.notAttempted) { setShowOverlay(true); } if (!assessment) { @@ -215,34 +217,21 @@ const AssessmentWorkspace: React.FC = props => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - /* - const handleReadOnlyMode = (event: KeyboardEvent) => { - // console.log(event.key); - if ( - !event.ctrlKey && - !event.altKey && - !event.metaKey && - event.key !== 'ArrowLeft' && - event.key !== 'ArrowRight' && - event.key !== 'ArrowUp' && - event.key !== 'ArrowDown' // Allow Meta (Command) key combinations for navigation on macOS - ) { - // console.log("Preventing default behavior of key press"); - event.stopPropagation(); - // Prevent default behavior of the key press if they change the editor - event.preventDefault(); - } - }; - useEffect(() => { - if (!isEditable) { - document.body.addEventListener('keydown', handleReadOnlyMode, true); - } - else { - document.body.removeEventListener('keydown', handleReadOnlyMode, true); + if (!isNull(activeEditorTabIndex)) { + const currentFilePath = editorTabs[activeEditorTabIndex].filePath; + if (currentFilePath && + currentFilePath === `/${workspaceLocation}/${questionId + 1}.js`) { + setIsEditable(true) + return; + } + } - }, [isEditable]) - */ + setIsEditable(false); + + + + }, [activeEditorTabIndex]) /** * Once there is an update (due to the assessment being fetched), check @@ -283,7 +272,6 @@ const AssessmentWorkspace: React.FC = props => { type: 'codeDelta', data: delta }; - console.log(input); pushLog(input); }; @@ -366,7 +354,7 @@ const AssessmentWorkspace: React.FC = props => { } /* Reset assessment if it has changed.*/ - const { assessmentId, questionId } = props; + if (storedAssessmentId === assessmentId && storedQuestionId === questionId) { return; } @@ -391,12 +379,18 @@ const AssessmentWorkspace: React.FC = props => { // We use || not ?? to match both null and an empty string // Sets the current active tab to the current "question file" and also force re-writes the file system + const someHardcodedFilesForTesting = { + "/assessment/test1.js": 'display("hello!");', + "/assessment/test2.js": '// just a comment here' + } // The leading slash "/" at the front is VERY IMPORTANT! DO NOT DELETE const currentQuestionFilePath = `/${workspaceLocation}/${questionId + 1}.js`; rewriteFilesWithContent(currentQuestionFilePath, { [currentQuestionFilePath]: - programmingQuestionData.answer || programmingQuestionData.solutionTemplate - }); + programmingQuestionData.answer || programmingQuestionData.solutionTemplate, + ...someHardcodedFilesForTesting + } + ); // Initialize session once the editorValue is known. @@ -687,7 +681,7 @@ const AssessmentWorkspace: React.FC = props => { key="folder" /> ); - + const fileModeButton = ( ); @@ -838,11 +832,6 @@ const AssessmentWorkspace: React.FC = props => { ); - /* If questionId is out of bounds, set it to the max. */ - const questionId = - props.questionId >= assessment.questions.length - ? assessment.questions.length - 1 - : props.questionId; const question = assessment.questions[questionId]; const editorContainerProps: NormalEditorContainerProps | undefined = question.type === QuestionTypes.programming || question.type === QuestionTypes.voting diff --git a/src/commons/fileSystemView/FileSystemViewFileNode.tsx b/src/commons/fileSystemView/FileSystemViewFileNode.tsx index 2af475714d..6b894e29fc 100644 --- a/src/commons/fileSystemView/FileSystemViewFileNode.tsx +++ b/src/commons/fileSystemView/FileSystemViewFileNode.tsx @@ -36,7 +36,6 @@ const FileSystemViewFileNode: React.FC = ( const handleOpenFile = () => { fileSystem.readFile(fullPath, 'utf-8', (err, fileContents) => { - console.log(fileContents) if (err) { console.error(err); } From ccc2edcbbe369f952ae1d19c453bc267ab2eaa6a Mon Sep 17 00:00:00 2001 From: = Date: Thu, 15 Feb 2024 23:45:57 +0800 Subject: [PATCH 13/46] Disable some control bar button in readonly mode --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 10 +++++----- src/commons/controlBar/ControlBarResetButton.tsx | 5 +++-- src/commons/controlBar/ControlBarRunButton.tsx | 3 ++- src/commons/controlBar/ControlBarSaveButton.tsx | 2 ++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 9ab988bb41..74ecaa1a3d 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -646,13 +646,14 @@ const AssessmentWorkspace: React.FC = props => { const resetButton = question.type !== QuestionTypes.mcq ? ( - + ) : null; const runButton = ( ); @@ -662,6 +663,7 @@ const AssessmentWorkspace: React.FC = props => { ) : null; @@ -691,10 +693,8 @@ const AssessmentWorkspace: React.FC = props => { ); - const editorButtonsMobileBreakpoint = isEditable - ? [fileModeButton, runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect] - : [fileModeButton, toggleFolderModeButton, chapterSelect]; - const editorButtonsNotMobileBreakpoint = isEditable ? [saveButton, resetButton] : []; + const editorButtonsMobileBreakpoint = [fileModeButton, runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect]; + const editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; const flowButtons = [previousButton, questionView, nextButton]; return { editorButtons: !isMobileBreakpoint diff --git a/src/commons/controlBar/ControlBarResetButton.tsx b/src/commons/controlBar/ControlBarResetButton.tsx index 4a299a853a..3a9cf2f233 100644 --- a/src/commons/controlBar/ControlBarResetButton.tsx +++ b/src/commons/controlBar/ControlBarResetButton.tsx @@ -5,8 +5,9 @@ import ControlButton from '../ControlButton'; type ControlBarResetButtonProps = { onClick?(): any; + disabled?: boolean; }; -export const ControlBarResetButton: React.FC = ({ onClick }) => { - return ; +export const ControlBarResetButton: React.FC = ({ onClick, disabled }) => { + return ; }; diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index c462356672..a2aae5e813 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -16,6 +16,7 @@ type StateProps = { isEntrypointFileDefined: boolean; color?: string; className?: string; + readOnly?: boolean; }; export const ControlBarRunButton: React.FC = props => { @@ -29,7 +30,7 @@ export const ControlBarRunButton: React.FC = props icon={IconNames.PLAY} onClick={props.handleEditorEval} options={{ iconColor: props.color, className: props.className }} - isDisabled={!props.isEntrypointFileDefined} + isDisabled={!props.isEntrypointFileDefined || props.readOnly} /> ); diff --git a/src/commons/controlBar/ControlBarSaveButton.tsx b/src/commons/controlBar/ControlBarSaveButton.tsx index fe87e59d36..c1616246b2 100644 --- a/src/commons/controlBar/ControlBarSaveButton.tsx +++ b/src/commons/controlBar/ControlBarSaveButton.tsx @@ -13,6 +13,7 @@ type DispatchProps = { type StateProps = { key: string; hasUnsavedChanges?: boolean; + disabled?: boolean; }; export const ControlButtonSaveButton: React.FC = props => { @@ -24,6 +25,7 @@ export const ControlButtonSaveButton: React.FC = prop icon={IconNames.FLOPPY_DISK} onClick={props.onClickSave} options={saveButtonOpts} + isDisabled={props.disabled} /> ); }; From 6e5c043807b6434d1481c8271ba02c8c27fcf960 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Thu, 15 Feb 2024 23:54:20 +0800 Subject: [PATCH 14/46] Reset to original question content after disabling folder mode --- .../AssessmentWorkspace.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 7cdea5a498..d5e5cd5a9d 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -189,6 +189,7 @@ const AssessmentWorkspace: React.FC = props => { dispatch(updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges)) }; }, [dispatch]); + const currentQuestionFilePath = `/${workspaceLocation}/${questionId + 1}.js`; /** * After mounting (either an older copy of the assessment @@ -225,13 +226,9 @@ const AssessmentWorkspace: React.FC = props => { setIsEditable(true) return; } - } setIsEditable(false); - - - - }, [activeEditorTabIndex]) + }, [activeEditorTabIndex, editorTabs, questionId]) /** * Once there is an update (due to the assessment being fetched), check @@ -347,7 +344,7 @@ const AssessmentWorkspace: React.FC = props => { * Checks if there is a need to reset the workspace, then executes * a dispatch (in the props) if needed. */ - const checkWorkspaceReset = () => { + const checkWorkspaceReset = (forced = false) => { /* Don't reset workspace if assessment not fetched yet. */ if (assessment === undefined) { return; @@ -355,7 +352,7 @@ const AssessmentWorkspace: React.FC = props => { /* Reset assessment if it has changed.*/ - if (storedAssessmentId === assessmentId && storedQuestionId === questionId) { + if ((storedAssessmentId === assessmentId && storedQuestionId === questionId) && !forced) { return; } @@ -384,7 +381,7 @@ const AssessmentWorkspace: React.FC = props => { "/assessment/test2.js": '// just a comment here' } // The leading slash "/" at the front is VERY IMPORTANT! DO NOT DELETE - const currentQuestionFilePath = `/${workspaceLocation}/${questionId + 1}.js`; + rewriteFilesWithContent(currentQuestionFilePath, { [currentQuestionFilePath]: programmingQuestionData.answer || programmingQuestionData.solutionTemplate, @@ -432,10 +429,6 @@ const AssessmentWorkspace: React.FC = props => { ); handleClearContext(question.library, true); 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. - //handleEditorValueChange(0, options.editorValue); - } }; /** @@ -677,7 +670,13 @@ const AssessmentWorkspace: React.FC = props => { isFolderModeEnabled={isFolderModeEnabled} isSessionActive={false} isPersistenceActive={false} - toggleFolderMode={() => dispatch(toggleFolderMode(workspaceLocation))} + toggleFolderMode={() => { + dispatch(toggleFolderMode(workspaceLocation)) + if (isFolderModeEnabled) { + // Set active tab back to default question if user disables folder mode + checkWorkspaceReset(true); + } + }} key="folder" /> ); From 3d249dbc9c70af66b13d8d2efb08502d80f8521c Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Thu, 15 Feb 2024 23:57:17 +0800 Subject: [PATCH 15/46] Disable handleEval for read-only mode --- .../AssessmentWorkspace.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 74ecaa1a3d..8c1334526a 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -118,7 +118,7 @@ const AssessmentWorkspace: React.FC = props => { const { isMobileBreakpoint } = useResponsive(); // isEditable is a placeholder for now. In the future, it should be set to be // based on whether it is the actual question being attempted. To enable read-only mode, set isEditable to false. - const isEditable = true; + const isEditable = false; const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); const [selectedTab, setSelectedTab] = useState( @@ -318,19 +318,21 @@ const AssessmentWorkspace: React.FC = props => { const activeTab = useRef(selectedTab); activeTab.current = selectedTab; const handleEval = useCallback(() => { - // Run testcases when the autograder tab is selected - if (activeTab.current === SideContentType.autograder) { - handleRunAllTestcases(); - } else { - handleEditorEval(); - } + if (isEditable) { + // Run testcases when the autograder tab is selected + if (activeTab.current === SideContentType.autograder) { + handleRunAllTestcases(); + } else { + handleEditorEval(); + } - const input: Input = { - time: Date.now(), - type: 'keyboardCommand', - data: KeyboardCommand.run - }; - pushLog(input); + const input: Input = { + time: Date.now(), + type: 'keyboardCommand', + data: KeyboardCommand.run + }; + pushLog(input); + } }, [handleEditorEval, handleRunAllTestcases, pushLog]); // Rewrites the file system with our desired file tree From 2c53b098dbaa38d5089090e64db7670861c4cfd2 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 16 Feb 2024 00:01:16 +0800 Subject: [PATCH 16/46] Fix reset --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index d5e5cd5a9d..39a88d0d97 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -817,11 +817,7 @@ const AssessmentWorkspace: React.FC = props => { label="Confirm" onClick={() => { closeOverlay(); - // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. - /*handleEditorValueChange( - 0, - (assessment!.questions[questionId] as IProgrammingQuestion).solutionTemplate - );*/ + checkWorkspaceReset(true); handleUpdateHasUnsavedChanges(true); }} options={{ minimal: false, intent: Intent.DANGER }} From b1975851068971985265f772b44f493f3572868e Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 16 Feb 2024 00:03:04 +0800 Subject: [PATCH 17/46] Linting --- .../AssessmentWorkspace.tsx | 27 +++++++++---------- .../fileSystemView/FileSystemViewFileNode.tsx | 11 ++++++-- .../fileSystemView/FileSystemViewList.tsx | 4 +-- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 39a88d0d97..df51bd1245 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -130,7 +130,6 @@ const AssessmentWorkspace: React.FC = props => { const navigate = useNavigate(); const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); - const { courseId } = useTypedSelector(state => state.session); const { @@ -221,14 +220,13 @@ const AssessmentWorkspace: React.FC = props => { useEffect(() => { if (!isNull(activeEditorTabIndex)) { const currentFilePath = editorTabs[activeEditorTabIndex].filePath; - if (currentFilePath && - currentFilePath === `/${workspaceLocation}/${questionId + 1}.js`) { - setIsEditable(true) + if (currentFilePath && currentFilePath === `/${workspaceLocation}/${questionId + 1}.js`) { + setIsEditable(true); return; } } setIsEditable(false); - }, [activeEditorTabIndex, editorTabs, questionId]) + }, [activeEditorTabIndex, editorTabs, questionId]); /** * Once there is an update (due to the assessment being fetched), check @@ -351,8 +349,8 @@ const AssessmentWorkspace: React.FC = props => { } /* Reset assessment if it has changed.*/ - - if ((storedAssessmentId === assessmentId && storedQuestionId === questionId) && !forced) { + + if (storedAssessmentId === assessmentId && storedQuestionId === questionId && !forced) { return; } @@ -377,17 +375,16 @@ const AssessmentWorkspace: React.FC = props => { // We use || not ?? to match both null and an empty string // Sets the current active tab to the current "question file" and also force re-writes the file system const someHardcodedFilesForTesting = { - "/assessment/test1.js": 'display("hello!");', - "/assessment/test2.js": '// just a comment here' - } + '/assessment/test1.js': 'display("hello!");', + '/assessment/test2.js': '// just a comment here' + }; // The leading slash "/" at the front is VERY IMPORTANT! DO NOT DELETE - + rewriteFilesWithContent(currentQuestionFilePath, { [currentQuestionFilePath]: programmingQuestionData.answer || programmingQuestionData.solutionTemplate, ...someHardcodedFilesForTesting - } - ); + }); // Initialize session once the editorValue is known. @@ -671,7 +668,7 @@ const AssessmentWorkspace: React.FC = props => { isSessionActive={false} isPersistenceActive={false} toggleFolderMode={() => { - dispatch(toggleFolderMode(workspaceLocation)) + dispatch(toggleFolderMode(workspaceLocation)); if (isFolderModeEnabled) { // Set active tab back to default question if user disables folder mode checkWorkspaceReset(true); @@ -680,7 +677,7 @@ const AssessmentWorkspace: React.FC = props => { key="folder" /> ); - + const fileModeButton = ( ); diff --git a/src/commons/fileSystemView/FileSystemViewFileNode.tsx b/src/commons/fileSystemView/FileSystemViewFileNode.tsx index 6b894e29fc..7d69bff18c 100644 --- a/src/commons/fileSystemView/FileSystemViewFileNode.tsx +++ b/src/commons/fileSystemView/FileSystemViewFileNode.tsx @@ -26,8 +26,15 @@ export type FileSystemViewFileNodeProps = { const FileSystemViewFileNode: React.FC = ( props: FileSystemViewFileNodeProps ) => { - const { workspaceLocation, fileSystem, basePath, fileName, indentationLevel, refreshDirectory, disableEditing } = - props; + const { + workspaceLocation, + fileSystem, + basePath, + fileName, + indentationLevel, + refreshDirectory, + disableEditing + } = props; const [isEditing, setIsEditing] = React.useState(false); const dispatch = useDispatch(); diff --git a/src/commons/fileSystemView/FileSystemViewList.tsx b/src/commons/fileSystemView/FileSystemViewList.tsx index 3f7fefedd1..8fda68c9c2 100644 --- a/src/commons/fileSystemView/FileSystemViewList.tsx +++ b/src/commons/fileSystemView/FileSystemViewList.tsx @@ -78,7 +78,7 @@ const FileSystemViewList: React.FC = (props: FileSystem {dirNames.map(dirName => { return ( = (props: FileSystem {fileNames.map(fileName => { return ( Date: Fri, 16 Feb 2024 00:11:20 +0800 Subject: [PATCH 18/46] Change ControlBarRunButton tooltip content when read-only --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 4 ++-- src/commons/controlBar/ControlBarRunButton.tsx | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 8c1334526a..524af85e1b 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -118,7 +118,7 @@ const AssessmentWorkspace: React.FC = props => { const { isMobileBreakpoint } = useResponsive(); // isEditable is a placeholder for now. In the future, it should be set to be // based on whether it is the actual question being attempted. To enable read-only mode, set isEditable to false. - const isEditable = false; + const isEditable = true; const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId)); const [selectedTab, setSelectedTab] = useState( @@ -333,7 +333,7 @@ const AssessmentWorkspace: React.FC = props => { }; pushLog(input); } - }, [handleEditorEval, handleRunAllTestcases, pushLog]); + }, [handleEditorEval, handleRunAllTestcases, pushLog, isEditable]); // Rewrites the file system with our desired file tree const rewriteFilesWithContent = async ( diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index a2aae5e813..f4497d2f63 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -21,7 +21,11 @@ type StateProps = { export const ControlBarRunButton: React.FC = props => { const tooltipContent = props.isEntrypointFileDefined - ? '...or press shift-enter in the editor' + ? ( + props.readOnly + ? 'Evaluation is disabled in read-only mode' + : '...or press shift-enter in the editor' + ) : 'Open a file to evaluate the program with the file as the entrypoint'; return ( From 13ac129b4bd3b8a17d23c50f264af1bee7689fdc Mon Sep 17 00:00:00 2001 From: accountexeregistere Date: Fri, 16 Feb 2024 00:16:21 +0800 Subject: [PATCH 19/46] Linting --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 15 +++++++++++++-- src/commons/controlBar/ControlBarResetButton.tsx | 9 +++++++-- src/commons/controlBar/ControlBarRunButton.tsx | 8 +++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 42e8f575c1..5fec4dc60e 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -633,7 +633,11 @@ const AssessmentWorkspace: React.FC = props => { const resetButton = question.type !== QuestionTypes.mcq ? ( - + ) : null; const runButton = ( @@ -686,7 +690,14 @@ const AssessmentWorkspace: React.FC = props => { ); - const editorButtonsMobileBreakpoint = [fileModeButton, runButton, saveButton, resetButton, toggleFolderModeButton, chapterSelect]; + const editorButtonsMobileBreakpoint = [ + fileModeButton, + runButton, + saveButton, + resetButton, + toggleFolderModeButton, + chapterSelect + ]; const editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; const flowButtons = [previousButton, questionView, nextButton]; return { diff --git a/src/commons/controlBar/ControlBarResetButton.tsx b/src/commons/controlBar/ControlBarResetButton.tsx index 3a9cf2f233..2cbf601714 100644 --- a/src/commons/controlBar/ControlBarResetButton.tsx +++ b/src/commons/controlBar/ControlBarResetButton.tsx @@ -8,6 +8,11 @@ type ControlBarResetButtonProps = { disabled?: boolean; }; -export const ControlBarResetButton: React.FC = ({ onClick, disabled }) => { - return ; +export const ControlBarResetButton: React.FC = ({ + onClick, + disabled +}) => { + return ( + + ); }; diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index f4497d2f63..9cd79468a0 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -21,11 +21,9 @@ type StateProps = { export const ControlBarRunButton: React.FC = props => { const tooltipContent = props.isEntrypointFileDefined - ? ( - props.readOnly - ? 'Evaluation is disabled in read-only mode' - : '...or press shift-enter in the editor' - ) + ? props.readOnly + ? 'Evaluation is disabled in read-only mode' + : '...or press shift-enter in the editor' : 'Open a file to evaluate the program with the file as the entrypoint'; return ( From 40c9ac9238333955fad23ae0d44b1558cb342968 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 16 Feb 2024 15:39:41 +0800 Subject: [PATCH 20/46] fix return to original file --- .../AssessmentWorkspace.tsx | 50 ++++++++++++++----- src/commons/fileSystem/utils.ts | 20 ++++++++ .../fileSystemView/FileSystemViewFileNode.tsx | 14 ++---- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 5fec4dc60e..dfe2159683 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -58,7 +58,7 @@ import { NormalEditorContainerProps } from '../editor/EditorContainer'; import { Position } from '../editor/EditorTypes'; -import { overwriteFilesInWorkspace } from '../fileSystem/utils'; +import { handleReadFile, overwriteFilesInWorkspace } from '../fileSystem/utils'; import FileSystemView from '../fileSystemView/FileSystemView'; import Markdown from '../Markdown'; import { MobileSideContentProps } from '../mobileWorkspace/mobileSideContent/MobileSideContent'; @@ -140,7 +140,7 @@ const AssessmentWorkspace: React.FC = props => { editorTestcases, hasUnsavedChanges, isRunning, - output, + output, replValue, sideContentHeight, currentAssessment: storedAssessmentId, @@ -301,7 +301,6 @@ const AssessmentWorkspace: React.FC = props => { const activeTab = useRef(selectedTab); activeTab.current = selectedTab; const handleEval = useCallback(() => { - if (isEditable) { // Run testcases when the autograder tab is selected if (activeTab.current === SideContentType.autograder) { handleRunAllTestcases(); @@ -315,10 +314,12 @@ const AssessmentWorkspace: React.FC = props => { data: KeyboardCommand.run }; pushLog(input); - } - }, [handleEditorEval, handleRunAllTestcases, pushLog, isEditable]); + }, [handleEditorEval, handleRunAllTestcases, pushLog]); - // Rewrites the file system with our desired file tree + /** + * Rewrites the file with our desired file tree + * Sets the currentQuestionFilePath to be the current active editor + */ const rewriteFilesWithContent = async ( currentQuestionFilePath: string, newFileTree: Record @@ -344,7 +345,7 @@ const AssessmentWorkspace: React.FC = props => { * Checks if there is a need to reset the workspace, then executes * a dispatch (in the props) if needed. */ - const checkWorkspaceReset = (forced = false) => { + const checkWorkspaceReset = (isReset = false) => { /* Don't reset workspace if assessment not fetched yet. */ if (assessment === undefined) { return; @@ -352,11 +353,13 @@ const AssessmentWorkspace: React.FC = props => { /* Reset assessment if it has changed.*/ - if (storedAssessmentId === assessmentId && storedQuestionId === questionId && !forced) { + if (storedAssessmentId === assessmentId && storedQuestionId === questionId && !isReset) { return; } const question = assessment.questions[questionId]; + // HARD CODED TO USE >= CHAPTER 2 FOR NOW [PLEASE REMOVE AFTER TESTING] + question.library.chapter = question.library.chapter === Chapter.SOURCE_1 ? Chapter.SOURCE_2 : question.library.chapter; const options: { autogradingResults?: AutogradingResult[]; @@ -377,13 +380,14 @@ const AssessmentWorkspace: React.FC = props => { // We use || not ?? to match both null and an empty string // Sets the current active tab to the current "question file" and also force re-writes the file system const someHardcodedFilesForTesting = { - '/assessment/test1.js': 'display("hello!");', + '/assessment/test1.js': `export const test = () => {display("hello");};`, '/assessment/test2.js': '// just a comment here' }; // The leading slash "/" at the front is VERY IMPORTANT! DO NOT DELETE rewriteFilesWithContent(currentQuestionFilePath, { [currentQuestionFilePath]: + isReset ? programmingQuestionData.solutionTemplate : programmingQuestionData.answer || programmingQuestionData.solutionTemplate, ...someHardcodedFilesForTesting }); @@ -663,7 +667,7 @@ const AssessmentWorkspace: React.FC = props => { {}} isFolderModeEnabled={isFolderModeEnabled} - sourceChapter={question.library.chapter} + sourceChapter={Chapter.SOURCE_2} sourceVariant={question.library.variant ?? Constants.defaultSourceVariant} disabled key="chapter" @@ -675,11 +679,31 @@ const AssessmentWorkspace: React.FC = props => { isFolderModeEnabled={isFolderModeEnabled} isSessionActive={false} isPersistenceActive={false} - toggleFolderMode={() => { + toggleFolderMode={async () => { dispatch(toggleFolderMode(workspaceLocation)); - if (isFolderModeEnabled) { + if (isFolderModeEnabled && fileSystem) { // Set active tab back to default question if user disables folder mode - checkWorkspaceReset(true); + let isFound = false; + editorTabs.forEach((tab, index) => { + if (tab.filePath && tab.filePath === currentQuestionFilePath) { + isFound = true; + dispatch(updateActiveEditorTabIndex(workspaceLocation, index)); + } + }) + // Original question tab not found, we need to open it + if (!isFound) { + const fileContents = await handleReadFile(fileSystem, currentQuestionFilePath); + console.log(fileContents) + dispatch( + addEditorTab( + workspaceLocation, + currentQuestionFilePath, + fileContents ?? '' + ) + ); + // Set to the end of the editorTabs array (i.e the newly created editorTab) + dispatch(updateActiveEditorTabIndex(workspaceLocation, editorTabs.length)); + } } }} key="folder" diff --git a/src/commons/fileSystem/utils.ts b/src/commons/fileSystem/utils.ts index 36ecd146d5..ed828f2d44 100644 --- a/src/commons/fileSystem/utils.ts +++ b/src/commons/fileSystem/utils.ts @@ -10,6 +10,26 @@ type File = { contents: string; }; +export const handleReadFile = ( + fileSystem: FSModule, + fullFilePath: string, +): Promise => { + return new Promise((resolve, reject) => { + fileSystem.readFile(fullFilePath, 'utf-8', (err, fileContents) => { + if (err) { + reject(err); + return; + } + if (fileContents === undefined) { + return; + } + + resolve(fileContents); + }); + }) + +}; + /** * Retrieves the files in the specified workspace as a record that maps from * file path to file content. Because BrowserFS lacks an equivalent to Node.js's diff --git a/src/commons/fileSystemView/FileSystemViewFileNode.tsx b/src/commons/fileSystemView/FileSystemViewFileNode.tsx index 7d69bff18c..8d5f1434a7 100644 --- a/src/commons/fileSystemView/FileSystemViewFileNode.tsx +++ b/src/commons/fileSystemView/FileSystemViewFileNode.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { useDispatch } from 'react-redux'; import classes from 'src/styles/FileSystemView.module.scss'; +import { handleReadFile } from '../fileSystem/utils'; import { showSimpleConfirmDialog } from '../utils/DialogHelper'; import { addEditorTab, removeEditorTabForFile } from '../workspace/WorkspaceActions'; import { WorkspaceLocation } from '../workspace/WorkspaceTypes'; @@ -41,17 +42,10 @@ const FileSystemViewFileNode: React.FC = ( const fullPath = path.join(basePath, fileName); - const handleOpenFile = () => { - fileSystem.readFile(fullPath, 'utf-8', (err, fileContents) => { - if (err) { - console.error(err); - } - if (fileContents === undefined) { - throw new Error('File contents are undefined.'); - } + const handleOpenFile = async () => { + const fileContents = await handleReadFile(fileSystem, fullPath); - dispatch(addEditorTab(workspaceLocation, fullPath, fileContents)); - }); + dispatch(addEditorTab(workspaceLocation, fullPath, fileContents)); }; const handleRenameFile = () => setIsEditing(true); From e7a5c7095095ac0490730f94d28a4d0c2d70417e Mon Sep 17 00:00:00 2001 From: = Date: Thu, 22 Feb 2024 22:58:39 +0800 Subject: [PATCH 21/46] Remove save button for read-only files --- .../AssessmentWorkspace.tsx | 39 +++++-------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 2845e5769b..469e4cb579 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -1,10 +1,9 @@ import { Button, + ButtonGroup, Card, Classes, Dialog, - DialogBody, - DialogFooter, Intent, NonIdealState, Spinner, @@ -713,7 +712,7 @@ const AssessmentWorkspace: React.FC = props => { ); - const editorButtonsMobileBreakpoint = [ + let editorButtonsMobileBreakpoint = [ fileModeButton, runButton, saveButton, @@ -721,8 +720,14 @@ const AssessmentWorkspace: React.FC = props => { toggleFolderModeButton, chapterSelect ]; - const editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; + let editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; const flowButtons = [previousButton, questionView, nextButton]; + + if (!isEditable) { + editorButtonsMobileBreakpoint = editorButtonsMobileBreakpoint.filter(x => x !== saveButton); + editorButtonsNotMobileBreakpoint = editorButtonsNotMobileBreakpoint.filter(x => x !== saveButton); + } + return { editorButtons: !isMobileBreakpoint ? editorButtonsMobileBreakpoint @@ -839,10 +844,9 @@ const AssessmentWorkspace: React.FC = props => { onClose={closeOverlay} title="Confirmation: Reset editor?" > - +
-<<<<<<< HEAD
@@ -858,29 +862,6 @@ const AssessmentWorkspace: React.FC = props => { />
-======= -
- - - { - closeOverlay(); - // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode. - handleEditorValueChange( - 0, - (assessment!.questions[questionId] as IProgrammingQuestion).solutionTemplate - ); - handleUpdateHasUnsavedChanges(true); - }} - options={{ minimal: false, intent: Intent.DANGER }} - /> - - } - /> ->>>>>>> f9c94503c89ff2b0ae05b216d5e23a28b9eb4d50 ); From 79604d349fc9eb522d279cc449a57617251ea693 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Thu, 22 Feb 2024 23:23:09 +0800 Subject: [PATCH 22/46] Only allow folder mode in assessments if source >= 2 selected --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 7559ae2b54..3a6f3be071 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -380,8 +380,7 @@ const AssessmentWorkspace: React.FC = props => { } const question = assessment.questions[questionId]; - // HARD CODED TO USE >= CHAPTER 2 FOR NOW [PLEASE REMOVE AFTER TESTING] - question.library.chapter = question.library.chapter === Chapter.SOURCE_1 ? Chapter.SOURCE_2 : question.library.chapter; + const options: { autogradingResults?: AutogradingResult[]; @@ -689,7 +688,7 @@ const AssessmentWorkspace: React.FC = props => { {}} isFolderModeEnabled={isFolderModeEnabled} - sourceChapter={Chapter.SOURCE_2} + sourceChapter={question.library.chapter} sourceVariant={question.library.variant ?? Constants.defaultSourceVariant} disabled key="chapter" @@ -741,9 +740,11 @@ const AssessmentWorkspace: React.FC = props => { runButton, saveButton, resetButton, - toggleFolderModeButton, chapterSelect ]; + + // Only allow folder mode to be enabled if chapter >= 2 + if (question.library.chapter >= 2) editorButtonsMobileBreakpoint.push(toggleFolderModeButton) const editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; const flowButtons = [previousButton, questionView, nextButton]; return { From d417d5ef605505b80d3096f1c614d1faba187570 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 23 Feb 2024 02:09:45 +0800 Subject: [PATCH 23/46] Move read-only icons to tabs --- .../AssessmentWorkspace.tsx | 12 +++++-- src/commons/editor/Editor.tsx | 1 + src/commons/editor/EditorContainer.tsx | 10 +++++- src/commons/editor/tabs/EditorTab.tsx | 8 ++++- .../editor/tabs/EditorTabContainer.tsx | 4 +++ src/commons/workspace/WorkspaceActions.ts | 8 +++++ src/commons/workspace/WorkspaceReducer.ts | 31 +++++++++++++++++++ src/commons/workspace/WorkspaceTypes.ts | 2 ++ 8 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 469e4cb579..d09ea44983 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -98,7 +98,8 @@ import { updateCurrentAssessmentId, updateEditorValue, updateHasUnsavedChanges, - updateReplValue + updateReplValue, + updateTabReadOnly } from '../workspace/WorkspaceActions'; import { WorkspaceLocation, WorkspaceState } from '../workspace/WorkspaceTypes'; import AssessmentWorkspaceGradingResult from './AssessmentWorkspaceGradingResult'; @@ -229,10 +230,12 @@ const AssessmentWorkspace: React.FC = props => { const currentFilePath = editorTabs[activeEditorTabIndex].filePath; if (currentFilePath && currentFilePath === `/${workspaceLocation}/${questionId + 1}.js`) { setIsEditable(true); + dispatch(updateTabReadOnly(workspaceLocation, activeEditorTabIndex, false)); return; } } setIsEditable(false); + dispatch(updateTabReadOnly(workspaceLocation, activeEditorTabIndex, true)); }, [activeEditorTabIndex, editorTabs, questionId]); /** @@ -725,9 +728,11 @@ const AssessmentWorkspace: React.FC = props => { if (!isEditable) { editorButtonsMobileBreakpoint = editorButtonsMobileBreakpoint.filter(x => x !== saveButton); - editorButtonsNotMobileBreakpoint = editorButtonsNotMobileBreakpoint.filter(x => x !== saveButton); + editorButtonsNotMobileBreakpoint = editorButtonsNotMobileBreakpoint.filter( + x => x !== saveButton + ); } - + return { editorButtons: !isMobileBreakpoint ? editorButtonsMobileBreakpoint @@ -865,6 +870,7 @@ const AssessmentWorkspace: React.FC = props => { ); + console.log(editorTabs.map(convertEditorTabStateToProps)); const question = assessment.questions[questionId]; const editorContainerProps: NormalEditorContainerProps | undefined = question.type === QuestionTypes.programming || question.type === QuestionTypes.voting diff --git a/src/commons/editor/Editor.tsx b/src/commons/editor/Editor.tsx index 70927a0270..d6f9f264fe 100644 --- a/src/commons/editor/Editor.tsx +++ b/src/commons/editor/Editor.tsx @@ -62,6 +62,7 @@ export type EditorTabStateProps = { highlightedLines: HighlightedLines[]; breakpoints: string[]; newCursorPosition?: Position; + readOnly?: boolean; }; type LocalStateProps = { diff --git a/src/commons/editor/EditorContainer.tsx b/src/commons/editor/EditorContainer.tsx index 8fbe71799d..6b40a06a84 100644 --- a/src/commons/editor/EditorContainer.tsx +++ b/src/commons/editor/EditorContainer.tsx @@ -46,7 +46,14 @@ export const convertEditorTabStateToProps = ( return { editorTabIndex, editorValue: editorTab.value, - ..._.pick(editorTab, 'filePath', 'highlightedLines', 'breakpoints', 'newCursorPosition') + ..._.pick( + editorTab, + 'filePath', + 'highlightedLines', + 'breakpoints', + 'newCursorPosition', + 'readOnly' + ) }; }; @@ -95,6 +102,7 @@ const EditorContainer: React.FC = (props: EditorContainerP
{isFolderModeEnabled && ( void; remove: () => void; + readOnly?: boolean; }; const EditorTab: React.FC = (props: EditorTabProps) => { - const { filePath, isActive, setActive, remove } = props; + const { filePath, isActive, setActive, remove, readOnly } = props; + console.log(readOnly); const onClick = (e: React.MouseEvent) => { // Stop the click event from propagating to the parent component. @@ -26,6 +28,10 @@ const EditorTab: React.FC = (props: EditorTabProps) => { })} onClick={setActive} > + {' '} + {readOnly !== undefined && ( + + )} {filePath} diff --git a/src/commons/editor/tabs/EditorTabContainer.tsx b/src/commons/editor/tabs/EditorTabContainer.tsx index b3c2fbe2f1..9ea892d75b 100644 --- a/src/commons/editor/tabs/EditorTabContainer.tsx +++ b/src/commons/editor/tabs/EditorTabContainer.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import { EditorTabStateProps } from '../Editor'; import EditorTab from './EditorTab'; import { getShortestUniqueFilePaths } from './utils'; export type EditorTabContainerProps = { + editorTabs: EditorTabStateProps[]; baseFilePath: string; filePaths: string[]; activeEditorTabIndex: number; @@ -13,6 +15,7 @@ export type EditorTabContainerProps = { const EditorTabContainer: React.FC = (props: EditorTabContainerProps) => { const { + editorTabs, baseFilePath, filePaths, activeEditorTabIndex, @@ -38,6 +41,7 @@ const EditorTabContainer: React.FC = (props: EditorTabC isActive={index === activeEditorTabIndex} setActive={() => setActiveEditorTabIndex(index)} remove={() => removeEditorTabByIndex(index)} + readOnly={editorTabs[index].readOnly} /> ))}
diff --git a/src/commons/workspace/WorkspaceActions.ts b/src/commons/workspace/WorkspaceActions.ts index d44890f0e7..93cc42cee2 100644 --- a/src/commons/workspace/WorkspaceActions.ts +++ b/src/commons/workspace/WorkspaceActions.ts @@ -62,6 +62,7 @@ import { UPDATE_CURRENT_ASSESSMENT_ID, UPDATE_CURRENT_SUBMISSION_ID, UPDATE_EDITOR_BREAKPOINTS, + UPDATE_EDITOR_TAB_READ_ONLY, UPDATE_EDITOR_VALUE, UPDATE_ENVSTEPS, UPDATE_ENVSTEPSTOTAL, @@ -343,6 +344,13 @@ export const renameEditorTabsForDirectory = createAction( }) ); +export const updateTabReadOnly = createAction( + UPDATE_EDITOR_TAB_READ_ONLY, + (workspaceLocation: WorkspaceLocation, editorTabIndex: number | null, isReadOnly: boolean) => ({ + payload: { workspaceLocation, editorTabIndex, isReadOnly } + }) +); + export const updateReplValue = createAction( UPDATE_REPL_VALUE, (newReplValue: string, workspaceLocation: WorkspaceLocation) => ({ diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 29fdcf3dbd..9efe2acc8c 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -1,4 +1,5 @@ import { stringify } from 'js-slang/dist/utils/stringify'; +import { isEqual, isNull } from 'lodash'; import { Reducer } from 'redux'; import { SourcecastReducer } from '../../features/sourceRecorder/sourcecast/SourcecastReducer'; @@ -72,6 +73,7 @@ import { UPDATE_CURRENT_ASSESSMENT_ID, UPDATE_CURRENT_SUBMISSION_ID, UPDATE_EDITOR_BREAKPOINTS, + UPDATE_EDITOR_TAB_READ_ONLY, UPDATE_EDITOR_VALUE, UPDATE_ENVSTEPS, UPDATE_ENVSTEPSTOTAL, @@ -1067,6 +1069,35 @@ export const WorkspaceReducer: Reducer = ( } }; } + case UPDATE_EDITOR_TAB_READ_ONLY: { + const { editorTabIndex, isReadOnly } = action.payload; + console.log(editorTabIndex, isReadOnly); + if (isNull(editorTabIndex) || editorTabIndex < 0) { + return state; + } + const editorTabs = state[workspaceLocation].editorTabs; + const newEditorTabs = editorTabs.map((editorTab: EditorTabState, index: number | null) => + index === editorTabIndex + ? { + ...editorTab, + readOnly: isReadOnly + } + : editorTab + ); + + if (isEqual(editorTabs, newEditorTabs)) { + return state; + } + + return { + ...state, + [workspaceLocation]: { + ...state[workspaceLocation], + editorTabs: newEditorTabs + } + }; + } + case UPDATE_REPL_VALUE: return { ...state, diff --git a/src/commons/workspace/WorkspaceTypes.ts b/src/commons/workspace/WorkspaceTypes.ts index 81998af787..b571ebc36d 100644 --- a/src/commons/workspace/WorkspaceTypes.ts +++ b/src/commons/workspace/WorkspaceTypes.ts @@ -56,6 +56,7 @@ export const REMOVE_EDITOR_TAB_FOR_FILE = 'REMOVE_EDITOR_TAB_FOR_FILE'; export const REMOVE_EDITOR_TABS_FOR_DIRECTORY = 'REMOVE_EDITOR_TABS_FOR_DIRECTORY'; export const RENAME_EDITOR_TAB_FOR_FILE = 'RENAME_EDITOR_TAB_FOR_FILE'; export const RENAME_EDITOR_TABS_FOR_DIRECTORY = 'RENAME_EDITOR_TABS_FOR_DIRECTORY'; +export const UPDATE_EDITOR_TAB_READ_ONLY = 'UPDATE_EDITOR_TAB_READ_ONLY'; export const UPDATE_HAS_UNSAVED_CHANGES = 'UPDATE_HAS_UNSAVED_CHANGES'; export const UPDATE_REPL_VALUE = 'UPDATE_REPL_VALUE'; export const UPDATE_WORKSPACE = 'UPDATE_WORKSPACE'; @@ -117,6 +118,7 @@ export type EditorTabState = { readonly highlightedLines: HighlightedLines[]; readonly breakpoints: string[]; readonly newCursorPosition?: Position; + readonly readOnly?: boolean; }; export type WorkspaceState = { From ed3c5a006c9324fc8306061e627515b7bed19885 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 23 Feb 2024 02:14:13 +0800 Subject: [PATCH 24/46] Fix incorrect tab lock-unlock icon --- src/commons/editor/tabs/EditorTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/editor/tabs/EditorTab.tsx b/src/commons/editor/tabs/EditorTab.tsx index 43dc27a7fc..c0c4641090 100644 --- a/src/commons/editor/tabs/EditorTab.tsx +++ b/src/commons/editor/tabs/EditorTab.tsx @@ -30,7 +30,7 @@ const EditorTab: React.FC = (props: EditorTabProps) => { > {' '} {readOnly !== undefined && ( - + )} {filePath} From d21dafb5b53c8f38dc751ab26258a86ca143788c Mon Sep 17 00:00:00 2001 From: = Date: Fri, 23 Feb 2024 02:22:49 +0800 Subject: [PATCH 25/46] Enable run for all files --- .../AssessmentWorkspace.tsx | 27 +++++++++---------- .../controlBar/ControlBarRunButton.tsx | 11 +++++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index d09ea44983..f9abd02d01 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -326,21 +326,19 @@ const AssessmentWorkspace: React.FC = props => { const activeTab = useRef(selectedTab); activeTab.current = selectedTab; const handleEval = useCallback(() => { - if (isEditable) { - // Run testcases when the autograder tab is selected - if (activeTab.current === SideContentType.autograder) { - handleRunAllTestcases(); - } else { - handleEditorEval(); - } - - const input: Input = { - time: Date.now(), - type: 'keyboardCommand', - data: KeyboardCommand.run - }; - pushLog(input); + // Run testcases when the autograder tab is selected + if (activeTab.current === SideContentType.autograder) { + handleRunAllTestcases(); + } else { + handleEditorEval(); } + + const input: Input = { + time: Date.now(), + type: 'keyboardCommand', + data: KeyboardCommand.run + }; + pushLog(input); }, [handleEditorEval, handleRunAllTestcases, pushLog, isEditable]); // Rewrites the file system with our desired file tree @@ -669,7 +667,6 @@ const AssessmentWorkspace: React.FC = props => { ); diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index 9cd79468a0..376bb84e63 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -16,15 +16,18 @@ type StateProps = { isEntrypointFileDefined: boolean; color?: string; className?: string; - readOnly?: boolean; }; export const ControlBarRunButton: React.FC = props => { - const tooltipContent = props.isEntrypointFileDefined - ? props.readOnly + const tooltipContent = props.isEntrypointFileDefined ? '...or press shift-enter in the editor' + : 'Open a file to evaluate the program with the file as the entrypoint'; + + /* + ? props.readOnly ? 'Evaluation is disabled in read-only mode' : '...or press shift-enter in the editor' : 'Open a file to evaluate the program with the file as the entrypoint'; + */ return ( = props icon={IconNames.PLAY} onClick={props.handleEditorEval} options={{ iconColor: props.color, className: props.className }} - isDisabled={!props.isEntrypointFileDefined || props.readOnly} + isDisabled={!props.isEntrypointFileDefined} /> ); From facf6b7212f9d819c8ebb21644e101e31e6868f1 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 23 Feb 2024 02:31:07 +0800 Subject: [PATCH 26/46] Remove file mode control bar button in AssessmentWorkspace, remove logs --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 10 ++++++---- src/commons/editor/tabs/EditorTab.tsx | 1 - src/commons/workspace/WorkspaceReducer.ts | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index f9abd02d01..087c1032eb 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -44,7 +44,7 @@ import { ControlBarProps } from '../controlBar/ControlBar'; import { ControlBarChapterSelect } from '../controlBar/ControlBarChapterSelect'; import { ControlBarClearButton } from '../controlBar/ControlBarClearButton'; import { ControlBarEvalButton } from '../controlBar/ControlBarEvalButton'; -import { ControlBarFileModeButton } from '../controlBar/ControlBarFileModeButton'; +// import { ControlBarFileModeButton } from '../controlBar/ControlBarFileModeButton'; import { ControlBarNextButton } from '../controlBar/ControlBarNextButton'; import { ControlBarPreviousButton } from '../controlBar/ControlBarPreviousButton'; import { ControlBarQuestionViewButton } from '../controlBar/ControlBarQuestionViewButton'; @@ -707,13 +707,16 @@ const AssessmentWorkspace: React.FC = props => { key="folder" /> ); - + + // Moved to tabs + /* const fileModeButton = ( ); + */ let editorButtonsMobileBreakpoint = [ - fileModeButton, + // fileModeButton, runButton, saveButton, resetButton, @@ -867,7 +870,6 @@ const AssessmentWorkspace: React.FC = props => { ); - console.log(editorTabs.map(convertEditorTabStateToProps)); const question = assessment.questions[questionId]; const editorContainerProps: NormalEditorContainerProps | undefined = question.type === QuestionTypes.programming || question.type === QuestionTypes.voting diff --git a/src/commons/editor/tabs/EditorTab.tsx b/src/commons/editor/tabs/EditorTab.tsx index c0c4641090..d64ec3dda9 100644 --- a/src/commons/editor/tabs/EditorTab.tsx +++ b/src/commons/editor/tabs/EditorTab.tsx @@ -13,7 +13,6 @@ export type EditorTabProps = { const EditorTab: React.FC = (props: EditorTabProps) => { const { filePath, isActive, setActive, remove, readOnly } = props; - console.log(readOnly); const onClick = (e: React.MouseEvent) => { // Stop the click event from propagating to the parent component. diff --git a/src/commons/workspace/WorkspaceReducer.ts b/src/commons/workspace/WorkspaceReducer.ts index 9efe2acc8c..de1d42d405 100644 --- a/src/commons/workspace/WorkspaceReducer.ts +++ b/src/commons/workspace/WorkspaceReducer.ts @@ -1071,7 +1071,6 @@ export const WorkspaceReducer: Reducer = ( } case UPDATE_EDITOR_TAB_READ_ONLY: { const { editorTabIndex, isReadOnly } = action.payload; - console.log(editorTabIndex, isReadOnly); if (isNull(editorTabIndex) || editorTabIndex < 0) { return state; } From 26929fd5f348d7bd1af850626e1e17e1ad578d74 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 23 Feb 2024 02:47:06 +0800 Subject: [PATCH 27/46] Merge branch 'read-only-workspace' of github.com:source-academy/frontend into read-only-workspace --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index a550452608..0dadc240a7 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -326,22 +326,6 @@ const AssessmentWorkspace: React.FC = props => { const activeTab = useRef(selectedTab); activeTab.current = selectedTab; const handleEval = useCallback(() => { -<<<<<<< HEAD - // Run testcases when the autograder tab is selected - if (activeTab.current === SideContentType.autograder) { - handleRunAllTestcases(); - } else { - handleEditorEval(); - } - - const input: Input = { - time: Date.now(), - type: 'keyboardCommand', - data: KeyboardCommand.run - }; - pushLog(input); - }, [handleEditorEval, handleRunAllTestcases, pushLog, isEditable]); -======= // Run testcases when the autograder tab is selected if (activeTab.current === SideContentType.autograder) { handleRunAllTestcases(); @@ -356,7 +340,6 @@ const AssessmentWorkspace: React.FC = props => { }; pushLog(input); }, [handleEditorEval, handleRunAllTestcases, pushLog]); ->>>>>>> 79604d349fc9eb522d279cc449a57617251ea693 /** * Rewrites the file with our desired file tree From 46242dc95ad8d41e51fde804d4a20fdc3f17a293 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 23 Feb 2024 11:44:18 +0800 Subject: [PATCH 28/46] Add answers from other files --- .../AssessmentWorkspace.tsx | 74 +++++++++---------- src/commons/sagas/WorkspaceSaga.ts | 1 + 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index 3a6f3be071..79678636fc 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -37,6 +37,7 @@ import { IMCQQuestion, IProgrammingQuestion, Library, + Question, QuestionTypes, Testcase } from '../assessment/AssessmentTypes'; @@ -143,7 +144,7 @@ const AssessmentWorkspace: React.FC = props => { editorTestcases, hasUnsavedChanges, isRunning, - output, + output, replValue, sideContentHeight, currentAssessment: storedAssessmentId, @@ -323,19 +324,19 @@ const AssessmentWorkspace: React.FC = props => { const activeTab = useRef(selectedTab); activeTab.current = selectedTab; const handleEval = useCallback(() => { - // Run testcases when the autograder tab is selected - if (activeTab.current === SideContentType.autograder) { - handleRunAllTestcases(); - } else { - handleEditorEval(); - } + // Run testcases when the autograder tab is selected + if (activeTab.current === SideContentType.autograder) { + handleRunAllTestcases(); + } else { + handleEditorEval(); + } - const input: Input = { - time: Date.now(), - type: 'keyboardCommand', - data: KeyboardCommand.run - }; - pushLog(input); + const input: Input = { + time: Date.now(), + type: 'keyboardCommand', + data: KeyboardCommand.run + }; + pushLog(input); }, [handleEditorEval, handleRunAllTestcases, pushLog]); /** @@ -380,7 +381,6 @@ const AssessmentWorkspace: React.FC = props => { } const question = assessment.questions[questionId]; - const options: { autogradingResults?: AutogradingResult[]; @@ -400,17 +400,24 @@ const AssessmentWorkspace: React.FC = props => { // We use || not ?? to match both null and an empty string // Sets the current active tab to the current "question file" and also force re-writes the file system - const someHardcodedFilesForTesting = { - '/assessment/test1.js': `export const test = () => {display("hello");};`, - '/assessment/test2.js': '// just a comment here' - }; // The leading slash "/" at the front is VERY IMPORTANT! DO NOT DELETE + // "otherFiles" refers to all other files that have an "answer" record + const otherFiles: Record = {}; + assessment.questions.forEach((question: Question, index) => { + if (question.answer && question.type === 'programming' && index !== questionId) { + otherFiles[`/${workspaceLocation}/${index + 1}.js`] = question.answer; + } + }); + question.library.chapter = Chapter.SOURCE_2; + rewriteFilesWithContent(currentQuestionFilePath, { - [currentQuestionFilePath]: - isReset ? programmingQuestionData.solutionTemplate : - programmingQuestionData.answer || programmingQuestionData.solutionTemplate, - ...someHardcodedFilesForTesting + [currentQuestionFilePath]: isReset + ? programmingQuestionData.solutionTemplate + : isReset + ? programmingQuestionData.solutionTemplate + : programmingQuestionData.answer || programmingQuestionData.solutionTemplate, + ...otherFiles }); // Initialize session once the editorValue is known. @@ -710,17 +717,12 @@ const AssessmentWorkspace: React.FC = props => { isFound = true; dispatch(updateActiveEditorTabIndex(workspaceLocation, index)); } - }) + }); // Original question tab not found, we need to open it if (!isFound) { const fileContents = await handleReadFile(fileSystem, currentQuestionFilePath); - console.log(fileContents) dispatch( - addEditorTab( - workspaceLocation, - currentQuestionFilePath, - fileContents ?? '' - ) + addEditorTab(workspaceLocation, currentQuestionFilePath, fileContents ?? '') ); // Set to the end of the editorTabs array (i.e the newly created editorTab) dispatch(updateActiveEditorTabIndex(workspaceLocation, editorTabs.length)); @@ -735,16 +737,12 @@ const AssessmentWorkspace: React.FC = props => { ); - const editorButtonsMobileBreakpoint = [ - fileModeButton, - runButton, - saveButton, - resetButton, - chapterSelect - ]; + const editorButtonsMobileBreakpoint = [fileModeButton, runButton, saveButton, resetButton]; + // Only allow folder mode to be enabled if chapter >= 2 and is a programming question + if (question.library.chapter >= 2 && question.type === 'programming') + editorButtonsMobileBreakpoint.push(toggleFolderModeButton); - // Only allow folder mode to be enabled if chapter >= 2 - if (question.library.chapter >= 2) editorButtonsMobileBreakpoint.push(toggleFolderModeButton) + editorButtonsMobileBreakpoint.push(chapterSelect); const editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; const flowButtons = [previousButton, questionView, nextButton]; return { diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index ad1bb1c3f7..076dd1600b 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -1164,6 +1164,7 @@ export function* evalCode( ? DisplayBufferService.attachConsole(workspaceLocation) : () => {}; + console.log(files) const { result, interrupted, paused } = yield race({ result: actionType === DEBUG_RESUME From f71154a8f837145fc4e62e013c9bd001fc818e00 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 23 Feb 2024 11:52:09 +0800 Subject: [PATCH 29/46] merge fixes --- src/commons/assessmentWorkspace/AssessmentWorkspace.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index f5a56830f5..ec711cc83d 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -746,12 +746,13 @@ const AssessmentWorkspace: React.FC = props => { // fileModeButton, runButton, saveButton, - resetButton, - chapterSelect + resetButton ]; // Only allow folder mode to be enabled if chapter >= 2 if (question.library.chapter >= 2) editorButtonsMobileBreakpoint.push(toggleFolderModeButton) + editorButtonsMobileBreakpoint.push(chapterSelect) + let editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; const flowButtons = [previousButton, questionView, nextButton]; From c9528aa3ceeadec81d2b61eeb95e21a5aad511b5 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 23 Feb 2024 14:49:59 +0800 Subject: [PATCH 30/46] Linting --- .../assessmentWorkspace/AssessmentWorkspace.tsx | 6 +++--- src/commons/controlBar/ControlBarRunButton.tsx | 5 +++-- src/commons/fileSystem/utils.ts | 10 +++------- src/commons/sagas/WorkspaceSaga.ts | 2 +- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index ec711cc83d..b8a2a54cbd 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -734,7 +734,7 @@ const AssessmentWorkspace: React.FC = props => { key="folder" /> ); - + // Moved to tabs /* const fileModeButton = ( @@ -750,8 +750,8 @@ const AssessmentWorkspace: React.FC = props => { ]; // Only allow folder mode to be enabled if chapter >= 2 - if (question.library.chapter >= 2) editorButtonsMobileBreakpoint.push(toggleFolderModeButton) - editorButtonsMobileBreakpoint.push(chapterSelect) + if (question.library.chapter >= 2) editorButtonsMobileBreakpoint.push(toggleFolderModeButton); + editorButtonsMobileBreakpoint.push(chapterSelect); let editorButtonsNotMobileBreakpoint = [saveButton, resetButton]; const flowButtons = [previousButton, questionView, nextButton]; diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index 376bb84e63..f9f20c3fa6 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -19,9 +19,10 @@ type StateProps = { }; export const ControlBarRunButton: React.FC = props => { - const tooltipContent = props.isEntrypointFileDefined ? '...or press shift-enter in the editor' + const tooltipContent = props.isEntrypointFileDefined + ? '...or press shift-enter in the editor' : 'Open a file to evaluate the program with the file as the entrypoint'; - + /* ? props.readOnly ? 'Evaluation is disabled in read-only mode' diff --git a/src/commons/fileSystem/utils.ts b/src/commons/fileSystem/utils.ts index ed828f2d44..34085163d4 100644 --- a/src/commons/fileSystem/utils.ts +++ b/src/commons/fileSystem/utils.ts @@ -10,10 +10,7 @@ type File = { contents: string; }; -export const handleReadFile = ( - fileSystem: FSModule, - fullFilePath: string, -): Promise => { +export const handleReadFile = (fileSystem: FSModule, fullFilePath: string): Promise => { return new Promise((resolve, reject) => { fileSystem.readFile(fullFilePath, 'utf-8', (err, fileContents) => { if (err) { @@ -23,11 +20,10 @@ export const handleReadFile = ( if (fileContents === undefined) { return; } - + resolve(fileContents); }); - }) - + }); }; /** diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index b50c20ffb4..5a555a3388 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -1164,7 +1164,7 @@ export function* evalCode( ? DisplayBufferService.attachConsole(workspaceLocation) : () => {}; - console.log(files) + console.log(files); const { result, interrupted, paused } = yield race({ result: actionType === DEBUG_RESUME From d4c01eeecc30844186d909df2223e32c26d6b5cc Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Fri, 23 Feb 2024 19:00:27 +0800 Subject: [PATCH 31/46] Automatically run question file for auto grader tab --- .../AssessmentWorkspace.tsx | 10 +- src/commons/sagas/WorkspaceSaga.ts | 141 ++++++++++++------ .../sideContent/SideContentAutograder.tsx | 16 +- .../subcomponents/GradingWorkspace.tsx | 1 + src/styles/_workspace.scss | 10 ++ 5 files changed, 132 insertions(+), 46 deletions(-) diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index b8a2a54cbd..018a60f912 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -327,8 +327,10 @@ const AssessmentWorkspace: React.FC = props => { const activeTab = useRef(selectedTab); activeTab.current = selectedTab; const handleEval = useCallback(() => { + console.log('handleeval') // Run testcases when the autograder tab is selected if (activeTab.current === SideContentType.autograder) { + console.log("running all test cases!") handleRunAllTestcases(); } else { handleEditorEval(); @@ -408,10 +410,14 @@ const AssessmentWorkspace: React.FC = props => { // "otherFiles" refers to all other files that have an "answer" record const otherFiles: Record = {}; assessment.questions.forEach((question: Question, index) => { - if (question.answer && question.type === 'programming' && index !== questionId) { + if (question.type === 'programming' && question.answer && index !== questionId) { otherFiles[`/${workspaceLocation}/${index + 1}.js`] = question.answer; } }); + + // ================================================ + // !!! REMOVE AFTER TESTING !!! + // ================================================ question.library.chapter = Chapter.SOURCE_2; rewriteFilesWithContent(currentQuestionFilePath, { @@ -539,6 +545,8 @@ const AssessmentWorkspace: React.FC = props => { body: ( { const prependCode = state.workspaces[workspaceLocation].programPrependValue; - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - const editorCode = state.workspaces[workspaceLocation].editorTabs[0].value; - return [prependCode, editorCode] as [string, string]; + const activeEditorTab = state.workspaces[workspaceLocation].activeEditorTabIndex; + const currentFileCode = + activeEditorTab !== null + ? state.workspaces[workspaceLocation].editorTabs[activeEditorTab].value + : state.workspaces[workspaceLocation].editorTabs[0].value; + + return [prependCode, currentFileCode] as [string, string]; }); - const [prepend, editorValue] = code; + const [prepend, currentFileCode] = code; // Deal with prepended code let autocompleteCode; let prependLength = 0; if (!prepend) { - autocompleteCode = editorValue; + autocompleteCode = currentFileCode; } else { prependLength = prepend.split('\n').length; - autocompleteCode = prepend + '\n' + editorValue; + autocompleteCode = prepend + '\n' + currentFileCode; } const [editorNames, displaySuggestions] = yield call( @@ -328,10 +332,14 @@ export default function* WorkspaceSaga(): SagaIterator { yield takeEvery(DEBUG_RESUME, function* (action: ReturnType) { const workspaceLocation = action.payload.workspaceLocation; - const code: string = yield select( - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - (state: OverallState) => state.workspaces[workspaceLocation].editorTabs[0].value + const activeEditorTabIndex: number | null = yield select( + (state: OverallState) => state.workspaces[workspaceLocation].activeEditorTabIndex ); + const code: string = yield select((state: OverallState) => { + return activeEditorTabIndex + ? state.workspaces[workspaceLocation].editorTabs[activeEditorTabIndex].value + : state.workspaces[workspaceLocation].editorTabs[0].value; + }); const execTime: number = yield select( (state: OverallState) => state.workspaces[workspaceLocation].execTime ); @@ -339,12 +347,12 @@ export default function* WorkspaceSaga(): SagaIterator { /** Clear the context, with the same chapter and externalSymbols as before. */ yield put(actions.clearReplOutput(workspaceLocation)); context = yield select((state: OverallState) => state.workspaces[workspaceLocation].context); - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + yield put(actions.setEditorHighlightedLines(workspaceLocation, activeEditorTabIndex ? activeEditorTabIndex : 0, [])); const codeFilePath = '/code.js'; const codeFiles = { [codeFilePath]: code }; + yield call( evalCode, codeFiles, @@ -361,7 +369,8 @@ export default function* WorkspaceSaga(): SagaIterator { context = yield select((state: OverallState) => state.workspaces[workspaceLocation].context); yield put(actions.clearReplOutput(workspaceLocation)); // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + const activeEditorTabIndex: number | null = yield select((state: OverallState) => state.workspaces[workspaceLocation].activeEditorTabIndex) + yield put(actions.setEditorHighlightedLines(workspaceLocation, activeEditorTabIndex ? activeEditorTabIndex : 0, [])); context.runtime.break = false; lastDebuggerResult = undefined; }); @@ -541,9 +550,12 @@ export default function* WorkspaceSaga(): SagaIterator { NAV_DECLARATION, function* (action: ReturnType) { const workspaceLocation = action.payload.workspaceLocation; + const activeEditorTabIndex: number | null = yield select((state: OverallState) => state.workspaces[workspaceLocation].activeEditorTabIndex) const code: string = yield select( // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - (state: OverallState) => state.workspaces[workspaceLocation].editorTabs[0].value + (state: OverallState) => activeEditorTabIndex ? + state.workspaces[workspaceLocation].editorTabs[activeEditorTabIndex].value : + state.workspaces[workspaceLocation].editorTabs[0].value ); context = yield select((state: OverallState) => state.workspaces[workspaceLocation].context); @@ -552,9 +564,10 @@ export default function* WorkspaceSaga(): SagaIterator { column: action.payload.cursorPosition.column }); if (result) { + const activeEditorTabIndex: number | null = yield select((state: OverallState) => state.workspaces[workspaceLocation].activeEditorTabIndex) // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. yield put( - actions.moveCursor(action.payload.workspaceLocation, 0, { + actions.moveCursor(action.payload.workspaceLocation, activeEditorTabIndex ? activeEditorTabIndex : 0, { row: result.start.line - 1, column: result.start.column }) @@ -568,7 +581,7 @@ export default function* WorkspaceSaga(): SagaIterator { function* (action: ReturnType) { const { workspaceLocation } = action.payload; - yield call(evalEditor, workspaceLocation); + yield call(evalEditor, workspaceLocation, true); const testcases: Testcase[] = yield select( (state: OverallState) => state.workspaces[workspaceLocation].editorTestcases @@ -596,18 +609,19 @@ export default function* WorkspaceSaga(): SagaIterator { let lastDebuggerResult: any; let lastNonDetResult: Result; function* updateInspector(workspaceLocation: WorkspaceLocation): SagaIterator { + const activeEditorTabIndex: number | null = yield select((state: OverallState) => state.workspaces[workspaceLocation].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(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + yield put(actions.setEditorHighlightedLines(workspaceLocation, activeEditorTabIndex ? activeEditorTabIndex : 0, [])); // 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(actions.setEditorHighlightedLines(workspaceLocation, 0, [[row, row]])); + yield put(actions.setEditorHighlightedLines(workspaceLocation, activeEditorTabIndex ? activeEditorTabIndex : 0, [[row, row]])); visualizeEnv(lastDebuggerResult); } catch (e) { // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + yield put(actions.setEditorHighlightedLines(workspaceLocation, activeEditorTabIndex ? activeEditorTabIndex : 0, [])); // 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. @@ -761,7 +775,8 @@ function* insertDebuggerStatements( } export function* evalEditor( - workspaceLocation: WorkspaceLocation + workspaceLocation: WorkspaceLocation, + isGraderTab=false ): Generator { const [ prepend, @@ -788,21 +803,29 @@ export function* evalEditor( state.fileSystem.inBrowserFileSystem, state.session.remoteExecutionSession ]); - + console.log("evalEditor") if (activeEditorTabIndex === null) { throw new Error('Cannot evaluate program without an entrypoint file.'); } const defaultFilePath = `${WORKSPACE_BASE_PATHS[workspaceLocation]}/program.js`; + let entrypointFilePath = editorTabs[activeEditorTabIndex].filePath ?? defaultFilePath; let files: Record; if (isFolderModeEnabled) { files = yield call(retrieveFilesInWorkspaceAsRecord, workspaceLocation, fileSystem); + if (workspaceLocation === "assessment" && isGraderTab) { + const questionNumber = yield select( + (state: OverallState) => state.workspaces.assessment.currentQuestion + ) + if (typeof questionNumber !== undefined) { + entrypointFilePath = `${WORKSPACE_BASE_PATHS[workspaceLocation]}/${questionNumber + 1}.js` + } + } } else { files = { [defaultFilePath]: editorTabs[activeEditorTabIndex].value }; } - const entrypointFilePath = editorTabs[activeEditorTabIndex].filePath ?? defaultFilePath; yield put(actions.addEvent([EventType.RUN_CODE])); @@ -867,16 +890,31 @@ export function* runTestCase( workspaceLocation: WorkspaceLocation, index: number ): Generator { - const [prepend, value, postpend, testcase]: [string, string, string, string] = yield select( - (state: OverallState) => { - const prepend = state.workspaces[workspaceLocation].programPrependValue; - const postpend = state.workspaces[workspaceLocation].programPostpendValue; - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - const value = state.workspaces[workspaceLocation].editorTabs[0].value; - const testcase = state.workspaces[workspaceLocation].editorTestcases[index].program; - return [prepend, value, postpend, testcase] as [string, string, string, string]; - } - ); + const [prepend, value, postpend, testcase, isFolderModeEnabled]: [ + string, + string, + string, + string, + boolean + ] = yield select((state: OverallState) => { + const activeEditorTabIndex = state.workspaces[workspaceLocation].activeEditorTabIndex; + + const isFolderModeEnabled = state.workspaces[workspaceLocation].isFolderModeEnabled; + const prepend = state.workspaces[workspaceLocation].programPrependValue; + const postpend = state.workspaces[workspaceLocation].programPostpendValue; + const value = + activeEditorTabIndex !== null + ? state.workspaces[workspaceLocation].editorTabs[activeEditorTabIndex].value + : state.workspaces[workspaceLocation].editorTabs[0].value; + const testcase = state.workspaces[workspaceLocation].editorTestcases[index].program; + return [prepend, value, postpend, testcase, isFolderModeEnabled] as [ + string, + string, + string, + string, + boolean + ]; + }); const type: TestcaseType = yield select( (state: OverallState) => state.workspaces[workspaceLocation].editorTestcases[index].type ); @@ -918,19 +956,34 @@ export function* runTestCase( // Then execute student program silently in the original workspace context const blockKey = String(random(1048576, 68719476736)); yield* blockExtraMethods(elevatedContext, context, execTime, workspaceLocation, blockKey); - const valueFilePath = '/value.js'; - const valueFiles = { - [valueFilePath]: value + let valueFileEntryPath = '/value.js'; + let valueFiles: Record = { + [valueFileEntryPath]: value }; - yield call( - evalCode, - valueFiles, - valueFilePath, - context, - execTime, - workspaceLocation, - EVAL_SILENT - ); + + // Populate valueFiles with the entire fileSystem if folder mode is enabled and is an assessment + // Always sets the entry path as the current question + if (isFolderModeEnabled && workspaceLocation === "assessment") { + console.log("In runtestcase and here!") + const questionNumber = yield select( + (state: OverallState) => state.workspaces.assessment.currentQuestion + ) + if (typeof questionNumber !== undefined) { + valueFileEntryPath = `${WORKSPACE_BASE_PATHS[workspaceLocation]}/${questionNumber + 1}.js` + } + const fileSystem = yield select((state: OverallState) => state.fileSystem.inBrowserFileSystem); + valueFiles = yield call(retrieveFilesInWorkspaceAsRecord, workspaceLocation, fileSystem); + } + + yield call( + evalCode, + valueFiles, + valueFileEntryPath, + context, + execTime, + workspaceLocation, + EVAL_SILENT + ); // Halt execution if the student's code in the editor results in an error if (context.errors.length) { @@ -1038,6 +1091,7 @@ export function* evalCode( actionType: string, storyEnv?: string ): SagaIterator { + console.log("evalCode") context.runtime.debuggerOn = (actionType === EVAL_EDITOR || actionType === DEBUG_RESUME) && context.chapter > 2; const isStoriesBlock = actionType === EVAL_STORY || workspaceLocation === 'stories'; @@ -1164,7 +1218,6 @@ export function* evalCode( ? DisplayBufferService.attachConsole(workspaceLocation) : () => {}; - console.log(files); const { result, interrupted, paused } = yield race({ result: actionType === DEBUG_RESUME diff --git a/src/commons/sideContent/SideContentAutograder.tsx b/src/commons/sideContent/SideContentAutograder.tsx index c7ba1997d0..9a4018b8c1 100644 --- a/src/commons/sideContent/SideContentAutograder.tsx +++ b/src/commons/sideContent/SideContentAutograder.tsx @@ -18,6 +18,8 @@ type DispatchProps = { type StateProps = { autogradingResults: AutogradingResult[]; testcases: Testcase[]; + currentFileBeingRan: string; + isFolderModeEnabled?: boolean; }; type OwnProps = { @@ -32,7 +34,14 @@ const SideContentAutograder: React.FunctionComponent const [showsTestcases, setTestcasesShown] = React.useState(true); const [showsResults, setResultsShown] = React.useState(true); - const { testcases, autogradingResults, handleTestcaseEval, workspaceLocation } = props; + const { + testcases, + autogradingResults, + handleTestcaseEval, + workspaceLocation, + currentFileBeingRan, + isFolderModeEnabled + } = props; const testcaseCards = React.useMemo( () => @@ -82,6 +91,11 @@ const SideContentAutograder: React.FunctionComponent return (
+ {isFolderModeEnabled && ( + + Entry point file for Testcases: {currentFileBeingRan} + + )}
- Opened: 18th July, 13:24 + Opened: 18th July, 05:24
- Due: 18th June, 13:24 + Due: 18th June, 05:24
- Opened: 18th June, 13:24 + Opened: 18th June, 05:24
- Due: 18th June, 13:24 + Due: 18th June, 05:24
- Opens: 18th June, 13:24 + Opens: 18th June, 05:24
- Opened: 18th July, 13:24 + Opened: 18th July, 05:24
- Due: 18th June, 13:24 + Due: 18th June, 05:24
- Opened: 18th June, 13:24 + Opened: 18th June, 05:24
- Due: 18th June, 13:24 + Due: 18th June, 05:24
- + + + + + + Folder + + +
+ + + + + Folder + + +
- + + + + + + Folder + + +
- + + + + +
- + + + + + + Folder + + +
on - 18th June, 13:24 + 18th June, 05:24
From 41439e648b5710042bfd17311785d2d18f254f1e Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Mon, 15 Apr 2024 16:29:04 +0800 Subject: [PATCH 41/46] Fix types in test --- src/commons/sideContent/__tests__/SideContentAutograder.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/commons/sideContent/__tests__/SideContentAutograder.tsx b/src/commons/sideContent/__tests__/SideContentAutograder.tsx index 715f7f2526..6d4d35b99f 100644 --- a/src/commons/sideContent/__tests__/SideContentAutograder.tsx +++ b/src/commons/sideContent/__tests__/SideContentAutograder.tsx @@ -83,6 +83,7 @@ test('Autograder renders placeholders correctly when testcases and results are e const props: SideContentAutograderProps = { autogradingResults: [], testcases: [], + currentFileBeingRan: "", workspaceLocation: 'assessment', handleTestcaseEval: (testcaseId: number) => {} }; @@ -107,6 +108,7 @@ test('Autograder renders placeholders correctly when testcases and results are e test('Autograder renders public testcases with different statuses correctly', () => { const props: SideContentAutograderProps = { autogradingResults: [], + currentFileBeingRan: 'test1.js', testcases: mockPublicTestcases, workspaceLocation: 'assessment', handleTestcaseEval: (testcaseId: number) => {} @@ -152,6 +154,7 @@ test('Autograder renders public testcases with different statuses correctly', () test('Autograder renders opaque testcases with different statuses correctly in AssessmentWorkspace', () => { const props: SideContentAutograderProps = { autogradingResults: [], + currentFileBeingRan: 'test1.js', testcases: mockOpaqueTestcases, workspaceLocation: 'assessment', handleTestcaseEval: (testcaseId: number) => {} @@ -181,6 +184,7 @@ test('Autograder renders opaque testcases with different statuses correctly in A test('Autograder renders opaque testcases with different statuses correctly in GradingWorkspace', () => { const props: SideContentAutograderProps = { autogradingResults: [], + currentFileBeingRan: 'test1.js', testcases: mockOpaqueTestcases, workspaceLocation: 'grading', handleTestcaseEval: (testcaseId: number) => {} @@ -212,6 +216,7 @@ test('Autograder renders opaque testcases with different statuses correctly in G test('Autograder renders secret testcases with different statuses correctly', () => { const props: SideContentAutograderProps = { autogradingResults: [], + currentFileBeingRan: 'test1.js', testcases: mockSecretTestcases, workspaceLocation: 'grading', handleTestcaseEval: (testcaseId: number) => {} @@ -251,6 +256,7 @@ test('Autograder renders secret testcases with different statuses correctly', () test('Autograder renders autograder results with different statuses correctly', () => { const props: SideContentAutograderProps = { autogradingResults: mockAutogradingResults, + currentFileBeingRan: 'test1.js', testcases: [], workspaceLocation: 'assessment', handleTestcaseEval: (testcaseId: number) => {} From 96b1ff23d3c594f9b2913d42c8d858612f1e1d33 Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Mon, 15 Apr 2024 16:43:26 +0800 Subject: [PATCH 42/46] fix prettier errors --- src/commons/sideContent/__tests__/SideContentAutograder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/sideContent/__tests__/SideContentAutograder.tsx b/src/commons/sideContent/__tests__/SideContentAutograder.tsx index 6d4d35b99f..a91d0e56a5 100644 --- a/src/commons/sideContent/__tests__/SideContentAutograder.tsx +++ b/src/commons/sideContent/__tests__/SideContentAutograder.tsx @@ -83,7 +83,7 @@ test('Autograder renders placeholders correctly when testcases and results are e const props: SideContentAutograderProps = { autogradingResults: [], testcases: [], - currentFileBeingRan: "", + currentFileBeingRan: '', workspaceLocation: 'assessment', handleTestcaseEval: (testcaseId: number) => {} }; From 949ecd431d420212baa93c13106eee0f9fc3eafc Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Mon, 15 Apr 2024 17:33:16 +0800 Subject: [PATCH 43/46] fix error --- src/commons/sagas/PersistenceSaga.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/sagas/PersistenceSaga.tsx b/src/commons/sagas/PersistenceSaga.tsx index e87e2f923d..5818c9dc82 100644 --- a/src/commons/sagas/PersistenceSaga.tsx +++ b/src/commons/sagas/PersistenceSaga.tsx @@ -262,7 +262,7 @@ export function* persistenceSaga(): SagaIterator { if (activeEditorTabIndex === null) { throw new Error('No active editor tab found.'); } - const code = editorTabs[activeEditorTabIndex].value; + const code = editorTabs[activeEditorTabIndex] ? editorTabs[activeEditorTabIndex].value : ""; const config: IPlaygroundConfig = { chapter, From 87b1d0cb8fed22ce32ca96b19d446033020b5f3a Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Mon, 15 Apr 2024 17:40:40 +0800 Subject: [PATCH 44/46] lint --- src/commons/sagas/PersistenceSaga.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/sagas/PersistenceSaga.tsx b/src/commons/sagas/PersistenceSaga.tsx index 5818c9dc82..1933165795 100644 --- a/src/commons/sagas/PersistenceSaga.tsx +++ b/src/commons/sagas/PersistenceSaga.tsx @@ -262,7 +262,7 @@ export function* persistenceSaga(): SagaIterator { if (activeEditorTabIndex === null) { throw new Error('No active editor tab found.'); } - const code = editorTabs[activeEditorTabIndex] ? editorTabs[activeEditorTabIndex].value : ""; + const code = editorTabs[activeEditorTabIndex] ? editorTabs[activeEditorTabIndex].value : ''; const config: IPlaygroundConfig = { chapter, From 0739bc39a994142a3e69bc97f79c3b8e07652a5c Mon Sep 17 00:00:00 2001 From: tkaixiang Date: Mon, 15 Apr 2024 18:56:09 +0800 Subject: [PATCH 45/46] fix --- src/commons/sagas/PersistenceSaga.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/sagas/PersistenceSaga.tsx b/src/commons/sagas/PersistenceSaga.tsx index 1933165795..f5b8458d51 100644 --- a/src/commons/sagas/PersistenceSaga.tsx +++ b/src/commons/sagas/PersistenceSaga.tsx @@ -132,7 +132,7 @@ export function* persistenceSaga(): SagaIterator { if (activeEditorTabIndex === null) { throw new Error('No active editor tab found.'); } - const code = editorTabs[activeEditorTabIndex].value; + const code = editorTabs[activeEditorTabIndex] ? editorTabs[activeEditorTabIndex].value : ''; const pickedDir: PickFileResult = yield call( pickFile, From 6195fa703278d3b97cb14f70e9c856cdc9f3f27a Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Tue, 16 Apr 2024 00:17:10 +0800 Subject: [PATCH 46/46] Revert unnecessary changes --- public/externalLibs/sound/soundToneMatrix.js | 6 +++--- src/commons/fileSystemView/FileSystemViewContextMenu.tsx | 1 - src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/public/externalLibs/sound/soundToneMatrix.js b/public/externalLibs/sound/soundToneMatrix.js index d0246757bf..8638a90378 100644 --- a/public/externalLibs/sound/soundToneMatrix.js +++ b/public/externalLibs/sound/soundToneMatrix.js @@ -36,7 +36,7 @@ var timeout_matrix; // for coloring the matrix accordingly while it's being played var timeout_color; -var timeout_objects = []; +var timeout_objects = new Array(); // vector_to_list returns a list that contains the elements of the argument vector // in the given order. @@ -54,7 +54,7 @@ function vector_to_list(vector) { function x_y_to_row_column(x, y) { var row = Math.floor((y - margin_length) / (square_side_length + distance_between_squares)); var column = Math.floor((x - margin_length) / (square_side_length + distance_between_squares)); - return [row, column]; + return Array(row, column); } // given the row number of a square, return the leftmost coordinate @@ -365,5 +365,5 @@ function clear_all_timeout() { clearTimeout(timeout_objects[i]); } - timeout_objects = []; + timeout_objects = new Array(); } diff --git a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx index 851d08c990..7de8c1ed10 100644 --- a/src/commons/fileSystemView/FileSystemViewContextMenu.tsx +++ b/src/commons/fileSystemView/FileSystemViewContextMenu.tsx @@ -7,7 +7,6 @@ import classes from 'src/styles/ContextMenu.module.scss'; type Props = { children?: JSX.Element; className?: string; - createNewFile?: () => void; createNewDirectory?: () => void; open?: () => void; diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index a89dd99c8e..29426be942 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -106,7 +106,6 @@ export function* evalCode( ); const entrypointCode = files[entrypointFilePath]; - const lastNonDetResult = yield select( (state: OverallState) => state.workspaces[workspaceLocation].lastNonDetResult );