From e181bf42537f289aaec8fac31f2bc678d26e8e51 Mon Sep 17 00:00:00 2001 From: Siyu Chen <44207825+syuChen1@users.noreply.github.com> Date: Wed, 11 Aug 2021 13:19:10 -0400 Subject: [PATCH] Release/v0.8 (#263) * fix: student view top menu scaling * unit next btn module * unit&lesson module updates * console initial code && filter serial port to show only adrino uno && updated react dependency * minor change to console view * TextDecoderStream not working * finished reader functionality * initial working console view * new interactions * able to select baud rate * able to select baud rate * added newLine, fixed string being chunk, added input initial code * input working. Trying to add scroll bar for console * console scroll bar, modified baud rate display * remove page scroll bar for better UX * added event listener for device disconnection. * updated UI * fix: filter unable to process unit number > 9 * feat: allow console to slide up on to canvas; added scrollbar to content creator toolbox * added disable choose baud rate * added device status message * student login auth * edit error message * fix bug where mentor/cc not able to upload code to arduino * added pop-up message * fixed LS_name in day * fixed Change button not showing on Classroom when there's no LS * added confirm button for unsave CC workspace * added serial connection auto open, edited connection message * updated button style * fixed update unix for cc. Not able to update grade * added message * ui updates * sorted file structure * updated Add Day functionality * fix: content creator toolbox prefilled with student options * feat: toggle select all now expand/contract toolbox * merge develop into pre_demo_fix * take out add day after add a lesson temporarily * added z-index to btn-container * fix merge bug && ant-table wrapper * fixed cc unit grade bug * Created Test workflow * clean up * comments and prop * content creator rewrite * fix add ls bug * fix bug where student always go to Pedro's classroom * fix bug where student always go to Pedro's classroom * clean up * Added token for checking out action * Deleted version release on path * putting ref back in It seems like it requires the workflow format to include the ref * updated new ref release * Changed label name to be github_token for warning error * sorted blockly category in the backend * sorted blockly category in the backend * clean up db dump file and added meaningful data * modify avrgirl to get rid of connection error on heroku * edited avrgirl serial connection, added hover for save, edited go back warming * added error message Co-authored-by: chensation Co-authored-by: chensation <32966177+chensation@users.noreply.github.com> Co-authored-by: Anna Le Co-authored-by: lilyh14 Co-authored-by: Michael Pascuzzi Co-authored-by: Lily Hinkeldey <35618637+lilyh14@users.noreply.github.com> --- .github/workflows/test.yml | 15 + client/package.json | 11 +- client/public/lib/avrgirl-arduino.global.js | 43 +- client/src/Utils/requests.js | 734 +- client/src/assets/style.less | 3 + .../BlocklyCanvasPanel/BlocklyCanvasPanel.js | 1128 +- .../BlocklyCanvasPanel/ConsoleModal.js | 165 + .../src/components/DayPanels/DayPanels.less | 95 +- .../components/DayPanels/consoleHelpers.js | 94 + client/src/components/DayPanels/helpers.js | 193 +- .../MentorSubHeader/MentorSubHeader.less | 3 - client/src/components/Message.js | 12 + client/src/views/Classroom/Home/Home.js | 175 +- .../Home/LearningStandardSelect/CheckUnits.js | 6 +- .../LearningStandardSelect.js | 324 +- .../views/ContentCreator/ContentCreator.js | 382 +- .../views/ContentCreator/ContentCreator.less | 26 + .../LearningStandardCreator.js | 386 +- .../LearningStandardCreator.less | 92 +- .../LearningStandardDayCreator/DayEditor.js | 259 +- .../ContentCreator/UnitCreator/UnitCreator.js | 316 +- .../UnitCreator/UnitCreator.less | 95 +- .../ContentCreator/UnitEditor/UnitEditor.js | 234 +- client/src/views/Dashboard/Dashboard.js | 5 +- client/src/views/Day/Day.js | 120 +- client/src/views/Student/Student.js | 114 +- client/src/views/StudentLogin/StudentLogin.js | 254 +- .../src/views/StudentLogin/StudentLogin.less | 60 +- .../views/StudentLogin/StudentLoginForm.js | 98 +- client/src/views/Workspace/Workspace.js | 97 +- scripts/development_db.dump | 6777 +----- server/api/block/services/block.js | 16 +- server/api/classroom/controllers/classroom.js | 452 +- .../controllers/learning-standard.js | 5 - server/yarn.lock | 18128 ++++++++-------- 35 files changed, 12691 insertions(+), 18226 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 client/src/components/DayPanels/BlocklyCanvasPanel/ConsoleModal.js create mode 100644 client/src/components/DayPanels/consoleHelpers.js create mode 100644 client/src/components/Message.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..43f303cd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: Test App +on: + pull_request: + branches: [ develop ] + types: [ opened, reopened ] +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + - name: Test App + uses: STEM-C/auto/mocks@v0.7.1 + with: + github_token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/client/package.json b/client/package.json index 5e7cf017..da2ca50e 100644 --- a/client/package.json +++ b/client/package.json @@ -6,8 +6,8 @@ "@craco/craco": "^5.6.4", "@material-ui/core": "^4.10.2", "@material-ui/icons": "^4.9.1", - "@testing-library/jest-dom": "^4.2.4", - "@testing-library/react": "^9.3.2", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^7.1.2", "antd": "^4.3.5", "axios": "^0.21.1", @@ -15,11 +15,12 @@ "cross-env": "^7.0.2", "emoji-picker-react": "^3.2.1", "http-server": "^0.12.3", - "react": "^16.13.1", - "react-dom": "^16.13.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", "react-papaparse": "^3.7.0", "react-router-dom": "^5.1.2", - "react-scripts": "3.4.1", + "react-scripts": "^4.0.3", + "util": "^0.12.4", "yarn": "^1.22.10" }, "scripts": { diff --git a/client/public/lib/avrgirl-arduino.global.js b/client/public/lib/avrgirl-arduino.global.js index a6776a0a..4a67a519 100644 --- a/client/public/lib/avrgirl-arduino.global.js +++ b/client/public/lib/avrgirl-arduino.global.js @@ -13121,8 +13121,6 @@ THE SOFTWARE. this.writer = null; this.reader = null; this.baudRate = this.options.baudRate; - this.requestOptions = this.options.requestOptions || {}; - if (this.options.autoOpen) this.open(); } @@ -13132,32 +13130,25 @@ THE SOFTWARE. .catch((error) => {if (callback) {return callback(error)}}); } - open(callback) { - window.navigator.serial.requestPort(this.requestOptions) - .then(serialPort => { - this.port = serialPort; - if (this.isOpen) return; - return this.port.open({ baudRate: this.baudRate || 57600 }); - }) - .then(() => this.writer = this.port.writable.getWriter()) - .then(() => this.reader = this.port.readable.getReader()) - .then(async () => { - this.emit('open'); - this.isOpen = true; - callback(null); - while (this.port.readable.locked) { - try { - const { value, done } = await this.reader.read(); - if (done) { - break; - } - this.emit('data', Buffer.from(value)); - } catch (e) { - console.error(e); + async open(callback) { + this.port = window['port'] + await this.port.open({ baudRate: this.baudRate || 57600 }) + this.writer = this.port.writable.getWriter() + this.reader = this.port.readable.getReader() + this.emit('open'); + this.isOpen = true; + callback(null); + while (this.port.readable.locked) { + try { + const { value, done } = await this.reader.read(); + if (done) { + break; } + this.emit('data', Buffer.from(value)); + } catch (e) { + console.error(e); } - }) - .catch(error => {callback(error)}); + } } async close(callback) { diff --git a/client/src/Utils/requests.js b/client/src/Utils/requests.js index 8763500a..4cc1f2ea 100644 --- a/client/src/Utils/requests.js +++ b/client/src/Utils/requests.js @@ -1,376 +1,380 @@ -import {server} from './hosts' -import axios from 'axios' -import {getToken} from "./AuthRequests"; +import { server } from './hosts'; +import axios from 'axios'; +import { getToken } from './AuthRequests'; const GET = 'GET'; const PUT = 'PUT'; const POST = 'POST'; -const DELETE = 'DELETE' +const DELETE = 'DELETE'; // all request functions should utilize makeRequest and return an obj with structure {data, err} -const makeRequest = async ({method, path, data, auth = false, error}) => { - let res = null; - let err = null; - const config = auth ? { +const makeRequest = async ({ method, path, data, auth = false, error }) => { + let res = null; + let err = null; + const config = auth + ? { headers: { - Authorization: - `Bearer ${getToken()}` - } - } : null; - - try { - switch (method) { - case GET: - res = (await axios.get(path, config)).data; - break; - case POST: - res = (await axios.post(path, data, config)).data; - break; - case PUT: - res = (await axios.put(path, data, config)).data; - break; - case DELETE: - res = (await axios.delete(path, config)).data; - break; - default: - throw Error('Invalid method.') - } - } catch (e) { - console.error(e); - err = error ? error : "An error occurred." + Authorization: `Bearer ${getToken()}`, + }, + } + : null; + + try { + switch (method) { + case GET: + res = (await axios.get(path, config)).data; + break; + case POST: + res = (await axios.post(path, data, config)).data; + break; + case PUT: + res = (await axios.put(path, data, config)).data; + break; + case DELETE: + res = (await axios.delete(path, config)).data; + break; + default: + throw Error('Invalid method.'); } + } catch (e) { + console.error(e); + err = error ? error : 'An error occurred.'; + } - return {data: res, err: err} + return { data: res, err: err }; }; -export const getDayToolboxAll = async () => ( - makeRequest({ - method: GET, - path: `${server}/sandbox/toolbox`, - error: "Toolbox could not be retrieved." - }) -); - -export const getDayToolbox = async (id) => ( - makeRequest({ - method: GET, - path: `${server}/days/toolbox/${id}`, - auth: true, - error: "Toolbox could not be retrieved." - }) -); - -export const getMentor = async () => ( - makeRequest({ - method: GET, - path: `${server}/classroom-managers/me`, - auth: true, - error: "Your classroom manager information could not be retrieved." - }) -); - -export const getClassroom = async (id) => ( - makeRequest({ - method: GET, - path: `${server}/classrooms/${id}`, - auth: true, - error: "Classroom information could not be retrieved" - }) -); - -export const getStudentClassroom = async () => ( - makeRequest({ - method: GET, - path: `${server}/classrooms/student`, - auth: true, - error: "Classroom information could not be retrieved" - }) -); - -export const getClassrooms = async (ids) => (Promise.all(ids.map(async id => (await getClassroom(id)).data))); - -export const getStudents = async (code) => ( - makeRequest({ - method: GET, - path: `${server}/classrooms/join/${code}`, - error: "Student info could not be retrieved." - }) -); - -export const postJoin = async (code, ids) => ( - makeRequest({ - method: POST, - path: `${server}/classrooms/join/${code}`, - data: { - "students": ids, - }, - error: "Login failed." - }) -); - -export const createDay = async (day,learningStandard) =>( - makeRequest({ - method: POST, - path: `${server}/days`, - data: { - "learning_standard": learningStandard, - "number": day, - "template": ')', - }, - auth: true, - error: "Login failed." - }) -); - -export const setEnrollmentStatus = async (id, enrolled) => ( - makeRequest({ - method: PUT, - path: `${server}/students/enrolled/${id}`, - data: { - "enrolled": enrolled - }, - auth: true, - error: "Failed to change enrollment status." - }) -); - -export const updateStudent = async (id, student) => ( - makeRequest({ - method: PUT, - path: `${server}/students/${id}`, - data: student, - auth: true, - error: "Failed to update student." - }) -); - -export const getUnits = async (id) => ( - makeRequest({ - method: GET, - path: `${server}/units?grade=${id}`, - auth: true, - error: "Failed to retrieve units." - }) -); - -export const getLearningStandard = async (id) => ( - makeRequest({ - method: GET, - path: `${server}/learning-standards/${id}`, - auth: true, - error: "Failed to retrieve learning standard." - }) -); - -export const getUnit = async(id)=>( - makeRequest({ - method: GET, - path: `${server}/units/${id}`, - auth: true, - error: "Failed to retrieve learning standard." - }) -) - - -export const getAllUnits = async()=>( - makeRequest({ - method: GET, - path: `${server}/units`, - auth: true, - error: "Failed to retrieve learning standard." - }) -) - -export const getLearningStandardcount = async () => ( - makeRequest({ - method: GET, - path: `${server}/learning-standards/count`, - auth: true, - error: "Failed to retrieve learning standard." - }) -); - -export const getLearningStandardAll = async () => ( - makeRequest({ - method: GET, - path: `${server}/learning-standards`, - auth: true, - error: "Failed to retrieve learning standard." - }) -); - -export const setSelection = async (classroom, learningStandard) => ( - makeRequest({ - method: POST, - path: `${server}/selections/`, - data: { - classroom: classroom, - learning_standard: learningStandard - }, - auth: true, - error: "Failed to set active learning standard." - }) -); - -export const saveWorkspace = async (day, workspace) => ( - makeRequest({ - method: POST, - path: `${server}/saves`, - data: { - day: day, - workspace: workspace - }, - auth: true, - error: 'Failed to save your workspace.' - }) -); - -export const getSaves = async (day) => ( - makeRequest({ - method: GET, - path: `${server}/saves/day/${day}`, - auth: true, - error: 'Past saves could not be retrieved.' - }) -); - -export const createSubmission = async (day, workspace, sketch, path, isAuth) => ( - makeRequest({ - method: POST, - path: `${server}${path}`, - data: { - day: day.id, - workspace: workspace, - board: "arduino:avr:uno", - sketch: sketch - }, - auth: isAuth, - error: 'Failed to create submission.' - }) -); - -export const getSubmission = async (submissionId, path, isAuth) => ( - makeRequest({ - method: GET, - path: `${server}${path}/${submissionId}`, - auth: isAuth, - error: "Failed to retrieve submission status" - }) -) - -export const addStudent = async (name, character, classroom) => ( - makeRequest({ - method: POST, - path: `${server}/students`, - data: { - name: name, - character: character, - classroom: classroom - }, - auth: true, - error: 'Failed to add student.' - }) -); - -export const addStudents = async (students, classroom) => ( - makeRequest({ - method: POST, - path: `${server}/students`, - data: {students: students, classroom: classroom}, - auth: true, - error: 'Failed to add students.' - }) -); - -export const deleteStudent = async (student) => ( - makeRequest({ - method: DELETE, - path: `${server}/students/${student}`, - auth: true, - error: 'Failed to delete student.' - }) -); - -export const updateDayTemplate = async (id, workspace) => ( - makeRequest({ - method: PUT, - path: `${server}/days/${id}`, - data: {template: workspace}, - auth: true, - error: 'Failed to update Day' - }) -) - -export const updateDay = async (id, workspace, blocksList) => ( - makeRequest({ - method: PUT, - path: `${server}/days/${id}`, - data: { - "template": workspace, - "blocks": blocksList - }, - auth: true, - error: 'Failed to update the toolbox for the day' - }) -) - -export const deleteDay = async (id) => ( - makeRequest({ - method: DELETE, - path: `${server}/days/${id}`, - auth: true, - error: 'Failed to delete day.' - }) -); - -export const deleteLearningStandard = async (id) => ( - makeRequest({ - method: DELETE, - path: `${server}/learning-standards/${id}`, - auth: true, - error: 'Failed to delete student.' - }) -); - -export const createLearningStandard = async (description,name,number,unit, teks) =>( - makeRequest({ - method: POST, - path: `${server}/learning-standards`, - data: { - "expectations": description, - "name": name, - "number": number, - "unit": unit, - "teks": teks - }, - auth: true, - error: "Login failed." - }) -); - -export const createUnit = async(number, name,teksID,teksDescrip,grade)=>( - makeRequest({ - method: POST, - path: `${server}/units`, - data: { - "number": parseInt(number, 10), - "name": name, - "grade": parseInt(grade, 10), - "teks_id": teksID, - "teks_description": teksDescrip, - }, - auth: true, - error: "Login failed." - }) - -) - - -export const getGrades = async() => ( - makeRequest({ - method: GET, - path: `${server}/grades`, - auth: true, - error: "Grades could not be retrieved" - }) -) \ No newline at end of file +export const getDayToolboxAll = async () => + makeRequest({ + method: GET, + path: `${server}/sandbox/toolbox`, + error: 'Toolbox could not be retrieved.', + }); + +export const getDayToolbox = async (id) => + makeRequest({ + method: GET, + path: `${server}/days/toolbox/${id}`, + auth: true, + error: 'Toolbox could not be retrieved.', + }); + +export const getMentor = async () => + makeRequest({ + method: GET, + path: `${server}/classroom-managers/me`, + auth: true, + error: 'Your classroom manager information could not be retrieved.', + }); + +export const getClassroom = async (id) => + makeRequest({ + method: GET, + path: `${server}/classrooms/${id}`, + auth: true, + error: 'Classroom information could not be retrieved', + }); + +export const getStudentClassroom = async () => + makeRequest({ + method: GET, + path: `${server}/classrooms/student`, + auth: true, + error: 'Classroom information could not be retrieved', + }); + +export const getClassrooms = async (ids) => + Promise.all(ids.map(async (id) => (await getClassroom(id)).data)); + +export const getStudents = async (code) => + makeRequest({ + method: GET, + path: `${server}/classrooms/join/${code}`, + error: 'Student info could not be retrieved.', + }); + +export const postJoin = async (code, ids) => + makeRequest({ + method: POST, + path: `${server}/classrooms/join/${code}`, + data: { + students: ids, + }, + error: 'Login failed.', + }); + +export const createDay = async (day, learningStandard) => + makeRequest({ + method: POST, + path: `${server}/days`, + data: { + learning_standard: learningStandard, + number: day, + template: ')', + }, + auth: true, + error: 'Login failed.', + }); + +export const setEnrollmentStatus = async (id, enrolled) => + makeRequest({ + method: PUT, + path: `${server}/students/enrolled/${id}`, + data: { + enrolled: enrolled, + }, + auth: true, + error: 'Failed to change enrollment status.', + }); + +export const updateStudent = async (id, student) => + makeRequest({ + method: PUT, + path: `${server}/students/${id}`, + data: student, + auth: true, + error: 'Failed to update student.', + }); + +export const getUnits = async (id) => + makeRequest({ + method: GET, + path: `${server}/units?grade=${id}`, + auth: true, + error: 'Failed to retrieve units.', + }); + +export const getLearningStandard = async (id) => + makeRequest({ + method: GET, + path: `${server}/learning-standards/${id}`, + auth: true, + error: 'Failed to retrieve learning standard.', + }); + +export const getUnit = async (id) => + makeRequest({ + method: GET, + path: `${server}/units/${id}`, + auth: true, + error: 'Failed to retrieve learning standard.', + }); + +export const getAllUnits = async () => + makeRequest({ + method: GET, + path: `${server}/units`, + auth: true, + error: 'Failed to retrieve learning standard.', + }); + +export const getLearningStandardcount = async () => + makeRequest({ + method: GET, + path: `${server}/learning-standards/count`, + auth: true, + error: 'Failed to retrieve learning standard.', + }); + +export const getLearningStandardAll = async () => + makeRequest({ + method: GET, + path: `${server}/learning-standards`, + auth: true, + error: 'Failed to retrieve learning standard.', + }); + +export const setSelection = async (classroom, learningStandard) => + makeRequest({ + method: POST, + path: `${server}/selections/`, + data: { + classroom: classroom, + learning_standard: learningStandard, + }, + auth: true, + error: 'Failed to set active learning standard.', + }); + +export const saveWorkspace = async (day, workspace) => + makeRequest({ + method: POST, + path: `${server}/saves`, + data: { + day: day, + workspace: workspace, + }, + auth: true, + error: 'Failed to save your workspace.', + }); + +export const getSaves = async (day) => + makeRequest({ + method: GET, + path: `${server}/saves/day/${day}`, + auth: true, + error: 'Past saves could not be retrieved.', + }); + +export const createSubmission = async (day, workspace, sketch, path, isAuth) => + makeRequest({ + method: POST, + path: `${server}${path}`, + data: { + day: day.id, + workspace: workspace, + board: 'arduino:avr:uno', + sketch: sketch, + }, + auth: isAuth, + error: 'Failed to create submission.', + }); + +export const getSubmission = async (submissionId, path, isAuth) => + makeRequest({ + method: GET, + path: `${server}${path}/${submissionId}`, + auth: isAuth, + error: 'Failed to retrieve submission status', + }); + +export const addStudent = async (name, character, classroom) => + makeRequest({ + method: POST, + path: `${server}/students`, + data: { + name: name, + character: character, + classroom: classroom, + }, + auth: true, + error: 'Failed to add student.', + }); + +export const addStudents = async (students, classroom) => + makeRequest({ + method: POST, + path: `${server}/students`, + data: { students: students, classroom: classroom }, + auth: true, + error: 'Failed to add students.', + }); + +export const deleteStudent = async (student) => + makeRequest({ + method: DELETE, + path: `${server}/students/${student}`, + auth: true, + error: 'Failed to delete student.', + }); + +export const updateDayTemplate = async (id, workspace) => + makeRequest({ + method: PUT, + path: `${server}/days/${id}`, + data: { template: workspace }, + auth: true, + error: 'Failed to update Day', + }); + +export const updateDay = async (id, workspace, blocksList) => + makeRequest({ + method: PUT, + path: `${server}/days/${id}`, + data: { + template: workspace, + blocks: blocksList, + }, + auth: true, + error: 'Failed to update the toolbox for the day', + }); + +export const deleteDay = async (id) => + makeRequest({ + method: DELETE, + path: `${server}/days/${id}`, + auth: true, + error: 'Failed to delete day.', + }); + +export const deleteLearningStandard = async (id) => + makeRequest({ + method: DELETE, + path: `${server}/learning-standards/${id}`, + auth: true, + error: 'Failed to delete student.', + }); + +export const createLearningStandard = async ( + description, + name, + number, + unit, + teks +) => + makeRequest({ + method: POST, + path: `${server}/learning-standards`, + data: { + expectations: description, + name: name, + number: number, + unit: unit, + teks: teks, + }, + auth: true, + error: 'Login failed.', + }); + +export const createUnit = async (number, name, teksID, teksDescrip, grade) => + makeRequest({ + method: POST, + path: `${server}/units`, + data: { + number: parseInt(number, 10), + name: name, + grade: parseInt(grade, 10), + teks_id: teksID, + teks_description: teksDescrip, + }, + auth: true, + error: 'Fail to create new unit.', + }); + +export const updateUnit = async ( + id, + number, + name, + teksID, + teksDescrip, + grade +) => + makeRequest({ + method: PUT, + path: `${server}/units/${id}`, + data: { + number: parseInt(number, 10), + name: name, + grade: parseInt(grade, 10), + teks_id: teksID, + teks_description: teksDescrip, + }, + auth: true, + error: 'Failed to update unit', + }); + +export const getGrades = async () => + makeRequest({ + method: GET, + path: `${server}/grades`, + auth: true, + error: 'Grades could not be retrieved', + }); + +export const getGrade = async (grade) => + makeRequest({ + method: GET, + path: `${server}/grades/${grade}`, + auth: true, + error: 'Grade could not be retrieved', + }); diff --git a/client/src/assets/style.less b/client/src/assets/style.less index d54f1f5a..c7089243 100644 --- a/client/src/assets/style.less +++ b/client/src/assets/style.less @@ -43,6 +43,9 @@ min-height: 100vh; overflow-x: hidden; } +.container::-webkit-scrollbar { + display: none; +} .vertical-container { margin: 1.5em; diff --git a/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js b/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js index 89e386f3..ef92a168 100644 --- a/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js +++ b/client/src/components/DayPanels/BlocklyCanvasPanel/BlocklyCanvasPanel.js @@ -1,480 +1,708 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Link } from "react-router-dom"; -import '../DayPanels.less' -import { compileArduinoCode, handleCreatorSaveDay, handleSave } from "../helpers"; -import { message, Spin, Menu, Checkbox, Row, Col, Input, Switch } from "antd"; -import { getSaves } from "../../../Utils/requests"; -import CodeModal from "./CodeModal"; -import VersionHistoryModal from "./VersionHistoryModal" +import { Link } from 'react-router-dom'; +import '../DayPanels.less'; +import { + compileArduinoCode, + handleCreatorSaveDay, + handleSave, +} from '../helpers'; +import { message, Spin, Menu, Checkbox, Row, Col, Input, Switch } from 'antd'; +import { getSaves } from '../../../Utils/requests'; +import CodeModal from './CodeModal'; +import ConsoleModal from './ConsoleModal'; +import VersionHistoryModal from './VersionHistoryModal'; +import { openConnection, disconnect } from '../consoleHelpers'; export default function BlocklyCanvasPanel(props) { - const [hoverXml, setHoverXml] = useState(false); - const [hoverArduino, setHoverArduino] = useState(false); - const [hoverCompile, setHoverCompile] = useState(false); - const [selectedCompile, setSelectedCompile] = useState(false); - const [saves, setSaves] = useState({}); - const [studentToolbox, setStudentToolbox] = useState([]); - const [lastSavedTime, setLastSavedTime] = useState(null); - const [lastAutoSave, setLastAutoSave] = useState(null); - const [searchFilter, setSearchFilter] = useState(''); - const [selectAll, setSelectAll] = useState(false); - const [openedToolBoxCategories, setOpenedToolBoxCategories] = useState([]); - const [selectedToolBoxCategories, setSelectedToolBoxCategories] = useState([]); - - const { day, homePath, handleGoBack, isStudent, isMentor, isContentCreator, lessonName } = props; - - const workspaceRef = useRef(null); - const dayRef = useRef(null); - const { SubMenu } = Menu; - - const setWorkspace = () => - workspaceRef.current = window.Blockly.inject('blockly-canvas', - { toolbox: document.getElementById('toolbox') } - ); - - const loadSave = selectedSave => { - try { - let toLoad = day.template; - if (selectedSave !== -1) { - - if (lastAutoSave && selectedSave === -2) { - toLoad = lastAutoSave.workspace; - setLastSavedTime(getFormattedDate(lastAutoSave.updated_at)); - } else if (saves.current && saves.current.id === selectedSave) { - toLoad = saves.current.workspace; - setLastSavedTime(getFormattedDate(saves.current.updated_at)); - } else { - const s = saves.past.find(save => save.id === selectedSave); - if (s) { - toLoad = s.workspace; - setLastSavedTime(getFormattedDate(s.updated_at)) - } else { - message.error('Failed to restore save.') - return - } - } - } else { - setLastSavedTime(null) - } - let xml = window.Blockly.Xml.textToDom(toLoad); - if (workspaceRef.current) workspaceRef.current.clear(); - window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current); - workspaceRef.current.clearUndo() - } catch (e) { - message.error('Failed to load save.') - } - }; - - useEffect(() => { - // automatically save workspace every min - setInterval(async () => { - if (isStudent && workspaceRef.current && dayRef.current) { - const res = await handleSave(dayRef.current.id, workspaceRef); - if (res.data) { - setLastAutoSave(res.data[0]); - setLastSavedTime(getFormattedDate(res.data[0].updated_at)) - } - } - }, 60000); - - // clean up - saves workspace and removes blockly div from DOM - return async () => { - if (isStudent && dayRef.current && workspaceRef.current) - await handleSave(dayRef.current.id, workspaceRef); - if (workspaceRef.current) workspaceRef.current.dispose(); - dayRef.current = null - } - }, [isStudent]); - - useEffect(() => { - // once the day state is set, set the workspace and save - const setUp = async () => { - dayRef.current = day; - if (!workspaceRef.current && day && Object.keys(day).length !== 0) { - setWorkspace(); - - if (!isStudent && !isMentor && !isContentCreator) return; - - let onLoadSave = null; - const res = await getSaves(day.id); - if (res.data) { - if (res.data.current) onLoadSave = res.data.current; - setSaves(res.data) - } else { - console.log(res.err) - } - - if (onLoadSave) { - let xml = window.Blockly.Xml.textToDom(onLoadSave.workspace); - window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current); - setLastSavedTime(getFormattedDate(onLoadSave.updated_at)); - } else if (day.template) { - let xml = window.Blockly.Xml.textToDom(day.template); - window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current) - } - - workspaceRef.current.clearUndo() - } - }; - setUp() - }, [day, isStudent, isMentor, isContentCreator]); - - const handleManualSave = async () => { - // save workspace then update load save options - const res = await handleSave(day.id, workspaceRef); - if (res.err) { - message.error(res.err) + const [hoverXml, setHoverXml] = useState(false); + const [hoverSave, setHoverSave] = useState(false); + const [hoverUndo, setHoverUndo] = useState(false); + const [hoverRedo, setHoverRedo] = useState(false); + const [hoverArduino, setHoverArduino] = useState(false); + const [hoverCompile, setHoverCompile] = useState(false); + const [hoverConsole, setHoverConsole] = useState(false); + const [showConsole, setShowConsole] = useState(false); + const [connectionOpen, setConnectionOpen] = useState(false); + const [selectedCompile, setSelectedCompile] = useState(false); + const [saves, setSaves] = useState({}); + const [studentToolbox, setStudentToolbox] = useState([]); + const [lastSavedTime, setLastSavedTime] = useState(null); + const [lastAutoSave, setLastAutoSave] = useState(null); + const [searchFilter, setSearchFilter] = useState(''); + const [selectAll, setSelectAll] = useState(false); + const [openedToolBoxCategories, setOpenedToolBoxCategories] = useState([]); + const [selectedToolBoxCategories, setSelectedToolBoxCategories] = useState( + [] + ); + + const { + day, + homePath, + handleGoBack, + isStudent, + isMentor, + isContentCreator, + lessonName, + } = props; + + const workspaceRef = useRef(null); + const dayRef = useRef(null); + const { SubMenu } = Menu; + + const setWorkspace = () => + (workspaceRef.current = window.Blockly.inject('blockly-canvas', { + toolbox: document.getElementById('toolbox'), + })); + + const loadSave = (selectedSave) => { + try { + let toLoad = day.template; + if (selectedSave !== -1) { + if (lastAutoSave && selectedSave === -2) { + toLoad = lastAutoSave.workspace; + setLastSavedTime(getFormattedDate(lastAutoSave.updated_at)); + } else if (saves.current && saves.current.id === selectedSave) { + toLoad = saves.current.workspace; + setLastSavedTime(getFormattedDate(saves.current.updated_at)); } else { - setLastSavedTime(getFormattedDate(res.data[0].updated_at)); - message.success('Workspace saved successfully.') + const s = saves.past.find((save) => save.id === selectedSave); + if (s) { + toLoad = s.workspace; + setLastSavedTime(getFormattedDate(s.updated_at)); + } else { + message.error('Failed to restore save.'); + return; + } } + } else { + setLastSavedTime(null); + } + let xml = window.Blockly.Xml.textToDom(toLoad); + if (workspaceRef.current) workspaceRef.current.clear(); + window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current); + workspaceRef.current.clearUndo(); + } catch (e) { + message.error('Failed to load save.'); + } + }; - const savesRes = await getSaves(day.id); - if (savesRes.data) setSaves(savesRes.data); + const handleCCGoBack = () => { + if ( + window.confirm( + 'All unsaved progress will be lost. Do you still want to go back?' + ) + ) + handleGoBack(); + }; + + useEffect(() => { + // automatically save workspace every min + setInterval(async () => { + if (isStudent && workspaceRef.current && dayRef.current) { + const res = await handleSave(dayRef.current.id, workspaceRef); + if (res.data) { + setLastAutoSave(res.data[0]); + setLastSavedTime(getFormattedDate(res.data[0].updated_at)); + } + } + }, 60000); + + // clean up - saves workspace and removes blockly div from DOM + return async () => { + if (isStudent && dayRef.current && workspaceRef.current) + await handleSave(dayRef.current.id, workspaceRef); + if (workspaceRef.current) workspaceRef.current.dispose(); + dayRef.current = null; }; + }, [isStudent]); + + useEffect(() => { + // once the day state is set, set the workspace and save + const setUp = async () => { + dayRef.current = day; + if (!workspaceRef.current && day && Object.keys(day).length !== 0) { + setWorkspace(); + + if (!isStudent && !isMentor && !isContentCreator) return; + + if (isContentCreator) { + let tempCategories = [], + tempToolBox = []; + day && + day.selectedToolbox && + day.selectedToolbox.forEach(([category, blocks]) => { + tempCategories.push(category); + tempToolBox = [ + ...tempToolBox, + ...blocks.map((block) => block.name), + ]; + }); + + setOpenedToolBoxCategories(tempCategories); + setStudentToolbox(tempToolBox); + } - const handleCreatorSave = async () => { - const res = handleCreatorSaveDay(day.id, workspaceRef, studentToolbox); - if (res.err) { - message.error(res.err) + let onLoadSave = null; + const res = await getSaves(day.id); + if (res.data) { + if (res.data.current) onLoadSave = res.data.current; + setSaves(res.data); } else { - message.success('Day saved successfully') + console.log(res.err); } - } - const handleSearchFilterChange = (value) => { - - let validCategories = []; - - if (value === "") { - validCategories = day && day.toolbox && day.toolbox.reduce( - (accume, [category, blocks]) => { - if (blocks.some(block => studentToolbox.includes(block.name))) { - return [...accume, category]; - } - else { return accume; } - }, [] - ); - } - else { - validCategories = day && day.toolbox && day.toolbox.reduce( - (accume, [category, blocks]) => { - if (blocks.some(block => block.name.includes(value))) { - return [...accume, category]; - } - else { return accume; } - }, [] - ); + if (onLoadSave) { + let xml = window.Blockly.Xml.textToDom(onLoadSave.workspace); + window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current); + setLastSavedTime(getFormattedDate(onLoadSave.updated_at)); + } else if (day.template) { + let xml = window.Blockly.Xml.textToDom(day.template); + window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current); } - setOpenedToolBoxCategories(validCategories); - setSearchFilter(value); - } - /** - * filters out blocks not in searchFilter - * @param {object} blocks {name, description} - */ - const applySearchFilter = (blocks) => { - - return blocks.filter(block => block.name.includes(searchFilter)); - + workspaceRef.current.clearUndo(); + } + }; + setUp(); + }, [day, isStudent, isMentor, isContentCreator]); + + const handleManualSave = async () => { + // save workspace then update load save options + const res = await handleSave(day.id, workspaceRef); + if (res.err) { + message.error(res.err); + } else { + setLastSavedTime(getFormattedDate(res.data[0].updated_at)); + message.success('Workspace saved successfully.'); } - /** - * select or deselect entire toolbox - * @param {object} event - */ - const handleSelectEntireToolBox = (event) => { - - if(event.target.checked){ - let tempToolBox = []; - let tempCategories = []; - day && day.toolbox && day.toolbox.forEach( - ([category, blocks]) => { - tempCategories.push(category); - tempToolBox = [...tempToolBox, ...blocks.map(block => block.name)] - } - ); + const savesRes = await getSaves(day.id); + if (savesRes.data) setSaves(savesRes.data); + }; - setSelectedToolBoxCategories(tempCategories); - setStudentToolbox(tempToolBox); - setSelectAll(true); - } - else{ - setStudentToolbox([]); - setSelectedToolBoxCategories([]); - setSelectAll(false); - } + const handleCreatorSave = async () => { + const res = handleCreatorSaveDay(day.id, workspaceRef, studentToolbox); + if (res.err) { + message.error(res.err); + } else { + message.success('Day saved successfully'); } - - /** - * select or deselect toolbox category - * @param {boolean} checked if the switch has just be checked or not - * @param {string} category the category being selected - * @param {[object]} blocks the avaliable blocks inside the category - * @param {object} event - */ - const handleSelectToolBoxCategory = (checked, category, blocks, event) => { - - event.stopPropagation(); //prevent the submenu from being clicked on - - let blockNames = blocks.map(block => block.name); - - if (checked) { - setSelectedToolBoxCategories([...selectedToolBoxCategories, category]) - setStudentToolbox([...studentToolbox, ...blockNames.filter(item => !studentToolbox.includes(item))]); - } - else { - setSelectedToolBoxCategories(selectedToolBoxCategories.filter(item => item !== category)) - setStudentToolbox(studentToolbox.filter(item => !blockNames.includes(item))); - setSelectAll(false); - } + }; + + const handleSearchFilterChange = (value) => { + let validCategories = []; + + if (value === '') { + validCategories = + day && + day.toolbox && + day.toolbox.reduce((accume, [category, blocks]) => { + if (blocks.some((block) => studentToolbox.includes(block.name))) { + return [...accume, category]; + } else { + return accume; + } + }, []); + } else { + validCategories = + day && + day.toolbox && + day.toolbox.reduce((accume, [category, blocks]) => { + if (blocks.some((block) => block.name.includes(value))) { + return [...accume, category]; + } else { + return accume; + } + }, []); } - /** - * handle selecting a single block - * @param {boolean} checked - * @param {string} blockName - * @param {string} category the category block belongs to - */ - const handleSelectToolBoxBlock = (checked, blockName, category) => { - - //reverse, checked = just unchecked, !check = just checked - if (checked) { - setStudentToolbox(studentToolbox.filter(item => item !== blockName)); - setSelectAll(false); - setSelectedToolBoxCategories(selectedToolBoxCategories.filter(x => x !== category)) - } - else { - setStudentToolbox([...studentToolbox, blockName]); - } + setOpenedToolBoxCategories(validCategories); + setSearchFilter(value); + }; + /** + * filters out blocks not in searchFilter + * @param {object} blocks {name, description} + */ + const applySearchFilter = (blocks) => { + return blocks.filter((block) => block.name.includes(searchFilter)); + }; + + /** + * select or deselect entire toolbox + * @param {object} event + */ + const handleSelectEntireToolBox = (event) => { + if (event.target.checked) { + let tempToolBox = []; + let tempCategories = []; + day && + day.toolbox && + day.toolbox.forEach(([category, blocks]) => { + tempCategories.push(category); + tempToolBox = [...tempToolBox, ...blocks.map((block) => block.name)]; + }); + + setSelectedToolBoxCategories(tempCategories); + setStudentToolbox(tempToolBox); + setSelectAll(true); + } else { + setStudentToolbox([]); + setSelectedToolBoxCategories([]); + setSelectAll(false); } - - const handleUndo = () => { - if (workspaceRef.current.undoStack_.length > 0) - workspaceRef.current.undo(false) - }; - - const handleRedo = () => { - if (workspaceRef.current.redoStack_.length > 0) - workspaceRef.current.undo(true) - }; - - const getFormattedDate = dt => { - const d = new Date(Date.parse(dt)); - const day = d.getDate(); - const month = d.getMonth() + 1; - const year = d.getFullYear(); - let hrs = d.getHours(); - const ampm = hrs >= 12 ? 'PM' : 'AM'; - hrs = hrs % 12; - hrs = hrs ? hrs : 12; - let min = d.getMinutes(); - min = min < 10 ? '0' + min : min; - let sec = d.getSeconds(); - sec = sec < 10 ? '0' + sec : sec; - return `${month}/${day}/${year}, ${hrs}:${min}:${sec} ${ampm}` - }; - - return ( - -
- -
-
+ }; + + /** + * select or deselect toolbox category + * @param {boolean} checked if the switch has just be checked or not + * @param {string} category the category being selected + * @param {[object]} blocks the avaliable blocks inside the category + * @param {object} event + */ + const handleSelectToolBoxCategory = (checked, category, blocks, event) => { + event.stopPropagation(); //prevent the submenu from being clicked on + + let blockNames = blocks.map((block) => block.name); + + if (checked) { + setSelectedToolBoxCategories([...selectedToolBoxCategories, category]); + setStudentToolbox([ + ...studentToolbox, + ...blockNames.filter((item) => !studentToolbox.includes(item)), + ]); + } else { + setSelectedToolBoxCategories( + selectedToolBoxCategories.filter((item) => item !== category) + ); + setStudentToolbox( + studentToolbox.filter((item) => !blockNames.includes(item)) + ); + setSelectAll(false); + } + }; + + /** + * handle selecting a single block + * @param {boolean} checked + * @param {string} blockName + * @param {string} category the category block belongs to + */ + const handleSelectToolBoxBlock = (checked, blockName, category) => { + //reverse, checked = just unchecked, !check = just checked + if (checked) { + setStudentToolbox(studentToolbox.filter((item) => item !== blockName)); + setSelectAll(false); + setSelectedToolBoxCategories( + selectedToolBoxCategories.filter((x) => x !== category) + ); + } else { + setStudentToolbox([...studentToolbox, blockName]); + } + }; + + const handleUndo = () => { + if (workspaceRef.current.undoStack_.length > 0) + workspaceRef.current.undo(false); + }; + + const handleRedo = () => { + if (workspaceRef.current.redoStack_.length > 0) + workspaceRef.current.undo(true); + }; + + const connectToPort = async () => { + const filters = [ + { usbVendorId: 0x2341, usbProductId: 0x0043 }, + { usbVendorId: 0x2341, usbProductId: 0x0001 }, + ]; + let port; + try { + port = await navigator.serial.requestPort({ filters }); + } catch (e) { + console.log(e); + return; + } + window['port'] = port; + }; + + const handleConsole = async () => { + if (!showConsole) { + if (typeof window['port'] === 'undefined') { + await connectToPort(); + } + if (typeof window['port'] === 'undefined') { + message.error('Fail to select serial device'); + return; + } + setShowConsole(true); + setConnectionOpen(true); + document.getElementById('connect-button').innerHTML = 'Disconnect'; + openConnection(9600, true); + } else { + setShowConsole(false); + if (connectionOpen) { + console.log('Close connection'); + disconnect(); + setConnectionOpen(false); + document.getElementById('connect-button').innerHTML = 'Connect'; + } + } + }; + + const handleCompile = async () => { + if (connectionOpen) { + message.error('Close Serial Monitor before uploading your code'); + } else { + if (typeof window['port'] === 'undefined') { + await connectToPort(); + } + if (typeof window['port'] === 'undefined') { + message.error('Fail to select serial device'); + return; + } + compileArduinoCode( + workspaceRef.current, + setSelectedCompile, + day, + isStudent + ); + } + }; + + const getFormattedDate = (dt) => { + const d = new Date(Date.parse(dt)); + const day = d.getDate(); + const month = d.getMonth() + 1; + const year = d.getFullYear(); + let hrs = d.getHours(); + const ampm = hrs >= 12 ? 'PM' : 'AM'; + hrs = hrs % 12; + hrs = hrs ? hrs : 12; + let min = d.getMinutes(); + min = min < 10 ? '0' + min : min; + let sec = d.getSeconds(); + sec = sec < 10 ? '0' + sec : sec; + return `${month}/${day}/${year}, ${hrs}:${min}:${sec} ${ampm}`; + }; + + return ( +
+
+
+ + + {lessonName ? lessonName : 'Program your Arduino...'} + + + + + - - {lessonName ? lessonName : "Program your Arduino..."} + {homePath ? ( + + + + - - - - - - - - {homePath ? - - - - - - : null} - {handleGoBack ? - - - - : null} - - - - - - {isStudent && lastSavedTime ? - `Last changes saved ${lastSavedTime}` - : null - } - - - - {isStudent ? - - - - - : null - } - {isContentCreator ? - - - - : null} - - - - - - - - -
- {!isStudent ? - - : null} - - compileArduinoCode(workspaceRef.current, setSelectedCompile, day, isStudent)} - className="fas fa-upload hvr-info" - onMouseEnter={() => setHoverCompile(true)} - onMouseLeave={() => setHoverCompile(false)}/> - {hoverCompile &&
Upload to Arduino
} -
- -
- -
+ ) : null} + {handleGoBack ? ( + + + ) : null}
- -
-
- {isContentCreator ? -
- Current Student Toolbox Selection - - } - onChange={e => handleSearchFilterChange(e.target.value)} - /> - - - Select All - - - setOpenedToolBoxCategories(keys)} + + + + + {isStudent && lastSavedTime + ? `Last changes saved ${lastSavedTime}` + : null} + + + + {isStudent ? ( + + + + + ) : null} + {isContentCreator ? ( + + + + ) : null} + + + + + + + +
+ {!isStudent ? ( + + ) : null} + + setHoverCompile(true)} + onMouseLeave={() => setHoverCompile(false)} + /> + {hoverCompile && ( +
+ Upload to Arduino +
+ )} + + handleConsole()} + className='fas fa-terminal hvr-info' + style={{ marginLeft: '6px' }} + onMouseEnter={() => setHoverConsole(true)} + onMouseLeave={() => setHoverConsole(false)} + /> + {hoverConsole && ( +
+ Show Serial Monitor +
+ )}
- : null} -
- - {/* This xml is for the blocks' menu we will provide. Here are examples on how to include categories and subcategories */} - + +
+
+ +
+
+
+ {isContentCreator ? ( +
+
+ Current Student Toolbox Selection + } + onChange={(e) => handleSearchFilterChange(e.target.value)} + /> + + Select All + + setOpenedToolBoxCategories(keys)} + > { - // Maps out block categories - day && day.toolbox && day.toolbox.map(([category, blocks]) => ( - - { - // maps out blocks in category - // eslint-disable-next-line - blocks.map((block) => { - return - }) - } - + // Maps out block categories + day && + day.toolbox && + day.toolbox.map(([category, blocks]) => ( + + {category} + {openedToolBoxCategories.some( + (c) => c === category + ) ? ( //check if the submenu is open + + + handleSelectToolBoxCategory( + checked, + category, + blocks, + event + ) + } + /> + + ) : null} + + } + > + { + //filter out blocks not in search term + applySearchFilter(blocks).map((block) => { + return ( + + -1 + ? true + : false + } + onClick={(e) => + handleSelectToolBoxBlock( + !e.target.checked, + block.name, + category + ) + } + > + {block.name} + + + ); + }) + } + )) } - - -
- ) -} \ No newline at end of file + +
+
+ ) : null} + +
+ + {/* This xml is for the blocks' menu we will provide. Here are examples on how to include categories and subcategories */} + + { + // Maps out block categories + day && + day.toolbox && + day.toolbox.map(([category, blocks]) => ( + + { + // maps out blocks in category + // eslint-disable-next-line + blocks.map((block) => { + return ( + + ); + }) + } + + )) + } + +
+ ); +} diff --git a/client/src/components/DayPanels/BlocklyCanvasPanel/ConsoleModal.js b/client/src/components/DayPanels/BlocklyCanvasPanel/ConsoleModal.js new file mode 100644 index 00000000..8cb45cfe --- /dev/null +++ b/client/src/components/DayPanels/BlocklyCanvasPanel/ConsoleModal.js @@ -0,0 +1,165 @@ +import { Button, Checkbox, Select, Input, message, Row, Col } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { openConnection, disconnect, writeToPort } from '../consoleHelpers'; +import Message from '../../Message'; + +message.config({ + duration: 2, + maxCount: 1, +}); + +export default function ConsoleModal(props) { + const [baudRate, setBaudRate] = useState(9600); + const [input, setInput] = useState(''); + const [newLine, setnewLine] = useState(true); + const [deviceDisconnect, setDeviceDisconnect] = useState(false); + + const { connectionOpen, setConnectionOpen } = props; + + useEffect(() => { + navigator.serial.addEventListener('disconnect', (e) => { + console.log('device disconnected'); + window.port = undefined; + console.log('cleaned'); + setConnectionOpen(false); + document.getElementById('connect-button').innerHTML = 'Connect'; + setDeviceDisconnect(true); + message.error('Device Disconnected'); + }); + navigator.serial.addEventListener('connect', (e) => { + console.log('device connected'); + setDeviceDisconnect(false); + message.success('Device Connected'); + }); + }, [deviceDisconnect, setConnectionOpen]); + + const handleConnect = async () => { + if (!connectionOpen) { + if (typeof window['port'] === 'undefined') { + const filters = [ + { usbVendorId: 0x2341, usbProductId: 0x0043 }, + { usbVendorId: 0x2341, usbProductId: 0x0001 }, + ]; + let port; + try { + port = await navigator.serial.requestPort({ filters }); + } catch (e) { + console.log(e); + return; + } + window['port'] = port; + } + setConnectionOpen(true); + setDeviceDisconnect(false); + document.getElementById('connect-button').innerHTML = 'Disconnect'; + openConnection(baudRate, newLine); + } else { + console.log('Close connection'); + disconnect(); + setConnectionOpen(false); + document.getElementById('connect-button').innerHTML = 'Connect'; + } + }; + + const handleChange = ({ value }) => { + setBaudRate(value); + }; + + const sendInput = () => { + if (!connectionOpen) { + window.alert('Connection not opened.'); + return; + } + console.log(input); + writeToPort(input); + }; + + return ( +
+
+ Baud Rate: + + + { + setnewLine(!newLine); + }} + style={{ marginLeft: '50px' }} + > + New Line + +
+
+

Waiting for input...

+
+ + + { + setInput(e.target.value); + }} + > + + + + + + {deviceDisconnect ? ( + + ) : ( + + )} + + + {connectionOpen ? ( + + ) : ( + + )} + + +
+ ); +} diff --git a/client/src/components/DayPanels/DayPanels.less b/client/src/components/DayPanels/DayPanels.less index 6a75efcf..6826d60a 100644 --- a/client/src/components/DayPanels/DayPanels.less +++ b/client/src/components/DayPanels/DayPanels.less @@ -4,7 +4,8 @@ body { margin: 0; } -h3, p { +h3, +p { margin: 0; } @@ -19,7 +20,7 @@ h3, p { } .compilePop { - margin-top: 11px !important; + margin-top: 12px !important; } .popup { @@ -47,31 +48,32 @@ h3, p { top: 12.5vh; } -.ModalCompile{ - top: 80%; +.ModalCompile { + top: 85%; right: 3vh; } -.ModalCompile2{ - top: 80%; +.ModalCompile2 { + top: 85%; right: 12vh; } -.ModalCompile3{ - top: 80%; +.ModalCompile3 { + top: 85%; right: 5vh; } +.ModalCompile4 { + top: 100%; +} #action-btn-container { - margin-top: 0.5vh; i { color: #colors[primary]; - + font-size: 32px; &:hover { color: #colors[secondary]; - } } @@ -80,19 +82,17 @@ h3, p { height: 35px; x: 0px; y: 0px; - + g { fill: #colors[primary]; } &:hover { - g { fill: #colors[secondary]; } } } - } #description-container { @@ -131,7 +131,14 @@ h3, p { background-color: #colors[tertiary]; border-radius: 10px; height: 100%; - max-height: 78vh; + overflow: hidden; + + #menu { + overflow-y: auto; + overflow-x: hidden; + height: 100%; + max-height: 78vh; + } } #bottom-container { @@ -150,6 +157,49 @@ h3, p { width: 95%; height: 100%; min-height: 68vh; + // position: relative; + + // transition: min-height 2s; + + // &.contract { + // min-height: 28vh; + // } +} + +#console-container { + background-color: white; + border: 1px solid #colors[secondary]; + border-radius: 5px; + margin: 0 auto; + position: absolute; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; + height: 0; + transition: 0.5s ease; + z-index: 10; + + &.open { + height: calc(29vh + 92px); + } +} + +#console-content { + color: white; + text-align: left; +} + +#content-container { + width: 98%; + border-radius: 5px; + height: 30vh; + padding: 0.5vh 1vh; + margin: 0 auto; + background-color: black; + overflow-y: scroll; + position: relative; + word-break: break-word; } #section-header { @@ -174,7 +224,7 @@ h3, p { color: #colors[text-secondary]; width: 45%; min-height: 5%; - font-size: .8em; + font-size: 0.8em; font-weight: bold; position: relative; left: -10px; @@ -264,7 +314,7 @@ h3, p { } #disabled-option { - opacity: .6; + opacity: 0.6; background-color: darken(#colors[tertiary], 10%); } } @@ -278,7 +328,7 @@ h3, p { #history-item { display: flex; justify-content: space-between; - margin-bottom: .5vh; + margin-bottom: 0.5vh; background: #colors[tertiary]; width: 100%; height: 8vh; @@ -300,4 +350,11 @@ h3, p { #category-switch { float: right; vertical-align: middle; -} \ No newline at end of file +} + +#console-message { + float: left; + margin: 0.5vh 2rem; + width: 39rem; + height: 32px; +} diff --git a/client/src/components/DayPanels/consoleHelpers.js b/client/src/components/DayPanels/consoleHelpers.js new file mode 100644 index 00000000..a4acdbe6 --- /dev/null +++ b/client/src/components/DayPanels/consoleHelpers.js @@ -0,0 +1,94 @@ +let port; +let reader; +let writer; +let readableStreamClosed; + +class LineBreakTransformer { + constructor() { + this.container = ''; + } + + transform(chunk, controller) { + this.container += chunk; + const lines = this.container.split('\r\n'); + this.container = lines.pop(); + lines.forEach((line) => controller.enqueue(line)); + } + + flush(controller) { + controller.enqueue(this.container); + } +} + +export const openConnection = async (baudRate_, newLine) => { + //requesting port on the pop up window. + port = window['port']; + + var options = { + baudRate: baudRate_, + parity: 'none', + dataBits: 8, + stopBits: 1, + bufferSize: 1024, + }; + + // connect to port on baudRate 9600. + await port.open(options); + console.log(`port opened at baud rate: ${baudRate_} `); + document.getElementById('console-content').innerHTML = ''; + readUntilClose(newLine); +}; + +const readUntilClose = async (newLine) => { + const textDecoder = new window.TextDecoderStream(); + readableStreamClosed = port.readable.pipeTo(textDecoder.writable); + // reader = textDecoder.readable.getReader(); + reader = textDecoder.readable + .pipeThrough(new window.TransformStream(new LineBreakTransformer())) + .getReader(); + + console.log('reader opened'); + let string = ''; + while (true) { + const { value, done } = await reader.read(); + if (done) { + // Allow the serial port to be closed later. + reader.releaseLock(); + break; + } + console.log(value); + if (!newLine) { + string += value; + document.getElementById('console-content').innerHTML = string; + } else { + let newP = document.createElement('p'); + newP.innerHTML = value; + newP.style.margin = 0; + document.getElementById('console-content').appendChild(newP); + newP.scrollIntoView(); + } + } +}; + +export const writeToPort = async (data) => { + const textEncoder = new window.TextEncoder(); + writer = port.writable.getWriter(); + data += '\n'; + await writer.write(textEncoder.encode(data)); + console.log(textEncoder.encode(data)); + writer.releaseLock(); +}; + +export const disconnect = async () => { + reader.cancel(); + await readableStreamClosed.catch(() => { + /* Ignore the error */ + }); + if (typeof writer !== 'undefined') { + const textEncoder = new window.TextEncoder(); + writer = port.writable.getWriter(); + await writer.write(textEncoder.encode('')); + await writer.close(); + } + await port.close(); +}; diff --git a/client/src/components/DayPanels/helpers.js b/client/src/components/DayPanels/helpers.js index 4fb56d4a..63e34676 100644 --- a/client/src/components/DayPanels/helpers.js +++ b/client/src/components/DayPanels/helpers.js @@ -1,116 +1,145 @@ -import { createSubmission, getSubmission, saveWorkspace, updateDay } from "../../Utils/requests"; -import {message} from "antd"; +import { + createSubmission, + getSubmission, + saveWorkspace, + updateDay, +} from '../../Utils/requests'; +import { message } from 'antd'; const AvrboyArduino = window.AvrgirlArduino; export const setLocalSandbox = (workspaceRef) => { - let workspaceDom = window.Blockly.Xml.workspaceToDom(workspaceRef) - let workspaceText = window.Blockly.Xml.domToText(workspaceDom) - const localActivity = JSON.parse(localStorage.getItem('sandbox-day')) + let workspaceDom = window.Blockly.Xml.workspaceToDom(workspaceRef); + let workspaceText = window.Blockly.Xml.domToText(workspaceDom); + const localActivity = JSON.parse(localStorage.getItem('sandbox-day')); - let lastActivity = {...localActivity, template: workspaceText} - localStorage.setItem('sandbox-day', JSON.stringify(lastActivity)) -} + let lastActivity = { ...localActivity, template: workspaceText }; + localStorage.setItem('sandbox-day', JSON.stringify(lastActivity)); +}; // Generates xml from blockly canvas export const getXml = (workspaceRef, shouldAlert = true) => { + const { Blockly } = window; - const {Blockly} = window - - let xml = Blockly.Xml.workspaceToDom(workspaceRef) - let xml_text = Blockly.Xml.domToText(xml) - if(shouldAlert) alert(xml_text) - return (xml_text) + let xml = Blockly.Xml.workspaceToDom(workspaceRef); + let xml_text = Blockly.Xml.domToText(xml); + if (shouldAlert) alert(xml_text); + return xml_text; }; // Generates javascript code from blockly canvas export const getJS = (workspaceRef) => { - window.Blockly.JavaScript.INFINITE_LOOP_TRAP = null; - let code = window.Blockly.JavaScript.workspaceToCode(workspaceRef); - alert(code); - return (code); + window.Blockly.JavaScript.INFINITE_LOOP_TRAP = null; + let code = window.Blockly.JavaScript.workspaceToCode(workspaceRef); + alert(code); + return code; }; // Generates Arduino code from blockly canvas export const getArduino = (workspaceRef, shouldAlert = true) => { - window.Blockly.Arduino.INFINITE_LOOP_TRAP = null; - let code = window.Blockly.Arduino.workspaceToCode(workspaceRef); - if (shouldAlert) alert(code); - return (code); + window.Blockly.Arduino.INFINITE_LOOP_TRAP = null; + let code = window.Blockly.Arduino.workspaceToCode(workspaceRef); + if (shouldAlert) alert(code); + return code; }; // Sends compiled arduino code to server and returns hex to flash board with -export const compileArduinoCode = async (workspaceRef, setSelectedCompile, day, isStudent) => { - setSelectedCompile(true); - const sketch = getArduino(workspaceRef, false); - let workspaceDom = window.Blockly.Xml.workspaceToDom(workspaceRef); - let workspaceText = window.Blockly.Xml.domToText(workspaceDom); - let path; - isStudent ? path = "/submissions" : path = "/sandbox/submission"; - - try { - // create an initial submission - const initialSubmission = await createSubmission(day, workspaceText, sketch, path, isStudent); - - // get the submission result when it's ready and flash the board - await getAndFlashSubmission(initialSubmission.data.id, path, isStudent, setSelectedCompile) - } catch (e) { - console.log(e.message) - } +export const compileArduinoCode = async ( + workspaceRef, + setSelectedCompile, + day, + isStudent +) => { + setSelectedCompile(true); + const sketch = getArduino(workspaceRef, false); + let workspaceDom = window.Blockly.Xml.workspaceToDom(workspaceRef); + let workspaceText = window.Blockly.Xml.domToText(workspaceDom); + let path; + isStudent ? (path = '/submissions') : (path = '/sandbox/submission'); + isStudent ? (day = day) : (day.id = undefined); + + try { + // create an initial submission + const initialSubmission = await createSubmission( + day, + workspaceText, + sketch, + path, + isStudent + ); + + // get the submission result when it's ready and flash the board + await getAndFlashSubmission( + initialSubmission.data.id, + path, + isStudent, + setSelectedCompile + ); + } catch (e) { + console.log(e.message); + } }; -const getAndFlashSubmission = async (id, path, isStudent, setSelectedCompile) => { - // get the submission - const response = await getSubmission(id, path, isStudent) - - // if the submission is not complete, try again later - if (response.data.status !== "COMPLETED") { - setTimeout(() => getAndFlashSubmission(id, path, isStudent, setSelectedCompile), 250) - return - } - setSelectedCompile(false) - // flash the board with the output - await flashArduino(response); -} +const getAndFlashSubmission = async ( + id, + path, + isStudent, + setSelectedCompile +) => { + // get the submission + const response = await getSubmission(id, path, isStudent); + + // if the submission is not complete, try again later + if (response.data.status !== 'COMPLETED') { + setTimeout( + () => getAndFlashSubmission(id, path, isStudent, setSelectedCompile), + 250 + ); + return; + } + setSelectedCompile(false); + // flash the board with the output + await flashArduino(response); +}; const flashArduino = async (response) => { - if (response.data) { - // converting base 64 to hex - if(response.data.success){ - let Hex = atob(response.data.hex).toString(); - - const avrgirl = new AvrboyArduino({ - board: "uno", - debug: true - }); - - avrgirl.flash(Hex, (err) => { - if (err) { - console.log(err); - } else { - console.log('done correctly.'); - } - }) - } else if (response.data.msg) { - message.warning(response.data.stderr) + if (response.data) { + // converting base 64 to hex + if (response.data.success) { + let Hex = atob(response.data.hex).toString(); + + const avrgirl = new AvrboyArduino({ + board: 'uno', + debug: true, + }); + + avrgirl.flash(Hex, (err) => { + if (err) { + console.log(err); + } else { + console.log('done correctly.'); } - } else { - message.error(response.err); + }); + } else if (response.data.stderr) { + message.error(response.data.stderr, 10); } -} + } else { + message.error(response.err); + } +}; // save current workspace export const handleSave = async (dayId, workspaceRef) => { - let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current); - let xml_text = window.Blockly.Xml.domToText(xml); - return await saveWorkspace(dayId, xml_text); + let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current); + let xml_text = window.Blockly.Xml.domToText(xml); + return await saveWorkspace(dayId, xml_text); }; export const handleCreatorSaveDay = async (dayId, workspaceRef, blocksList) => { - let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current); - let xml_text = window.Blockly.Xml.domToText(xml); + let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current); + let xml_text = window.Blockly.Xml.domToText(xml); - console.log("The current blocksList is: ", blocksList) + console.log('The current blocksList is: ', blocksList); - return await updateDay(dayId, xml_text, blocksList); -} \ No newline at end of file + return await updateDay(dayId, xml_text, blocksList); +}; diff --git a/client/src/components/MentorSubHeader/MentorSubHeader.less b/client/src/components/MentorSubHeader/MentorSubHeader.less index 75757347..c7dd7646 100644 --- a/client/src/components/MentorSubHeader/MentorSubHeader.less +++ b/client/src/components/MentorSubHeader/MentorSubHeader.less @@ -6,7 +6,6 @@ padding-bottom: 2vh; h1 { - color: #colors[text-secondary]; margin-bottom: 0; float: left; position: relative; @@ -14,8 +13,6 @@ align-items: center; justify-content: center; color: #colors[text-secondary]; - margin-bottom: 0; - float: left; min-height: 6vh; width: 34%; border-radius: 80px; diff --git a/client/src/components/Message.js b/client/src/components/Message.js new file mode 100644 index 00000000..7c7ad556 --- /dev/null +++ b/client/src/components/Message.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { Alert } from 'antd'; + +const Message = ({ type, message }) => { + return ; +}; + +Message.defaultProps = { + type: 'error', +}; + +export default Message; diff --git a/client/src/views/Classroom/Home/Home.js b/client/src/views/Classroom/Home/Home.js index bdb0842b..1fa3feea 100644 --- a/client/src/views/Classroom/Home/Home.js +++ b/client/src/views/Classroom/Home/Home.js @@ -1,86 +1,107 @@ -import React, {useEffect, useState} from "react" -import "./Home.less" -import {getClassroom, getLearningStandard} from "../../../Utils/requests"; -import MentorSubHeader from "../../../components/MentorSubHeader/MentorSubHeader"; -import DisplayCodeModal from "./DisplayCodeModal"; -import LearningStandardModal from "./LearningStandardSelect/LearningStandardModal"; -import {message, Modal, Button} from "antd"; - +import React, { useEffect, useState } from 'react'; +import './Home.less'; +import { getClassroom, getLearningStandard } from '../../../Utils/requests'; +import MentorSubHeader from '../../../components/MentorSubHeader/MentorSubHeader'; +import DisplayCodeModal from './DisplayCodeModal'; +import LearningStandardModal from './LearningStandardSelect/LearningStandardModal'; +import { message, Modal, Button } from 'antd'; export default function Home(props) { - const [classroom, setClassroom] = useState({}); - const [gradeId, setGradeId] = useState(null); - const [activeLearningStandard, setActiveLearningStandard] = useState(null); - const {classroomId, history, viewing} = props; + const [classroom, setClassroom] = useState({}); + const [gradeId, setGradeId] = useState(null); + const [activeLearningStandard, setActiveLearningStandard] = useState(null); + const { classroomId, history, viewing } = props; - useEffect(() => { - const fetchData = async () => { - const res = await getClassroom(classroomId); - if(res.data){ - const classroom = res.data; - setClassroom(classroom); - setGradeId(classroom.grade.id); - classroom.selections.forEach(async selection => { - if (selection.current) { - const res = await getLearningStandard(selection.learning_standard); - if(res.data) setActiveLearningStandard(res.data); - else { - message.error(res.err); - } - } - }) - } else { - message.error(res.err); + useEffect(() => { + const fetchData = async () => { + const res = await getClassroom(classroomId); + if (res.data) { + const classroom = res.data; + setClassroom(classroom); + setGradeId(classroom.grade.id); + classroom.selections.forEach(async (selection) => { + if (selection.current) { + const res = await getLearningStandard(selection.learning_standard); + if (res.data) setActiveLearningStandard(res.data); + else { + message.error(res.err); } - }; - fetchData(); - }, [classroomId]); - - const handleViewDay = day => { - localStorage.setItem("my-day", JSON.stringify(day)); - history.push('/day') + } + }); + } else { + message.error(res.err); + } }; + fetchData(); + }, [classroomId]); - const handleBack = () => { - history.push("/dashboard"); - }; + const handleViewDay = (day, name) => { + day.learning_standard_name = name; + localStorage.setItem('my-day', JSON.stringify(day)); + history.push('/day'); + }; - return ( -
-