From 7b1faa38cb1605087bee827ff644c63d996e816c Mon Sep 17 00:00:00 2001 From: wltan <53135010+wltan@users.noreply.github.com> Date: Wed, 19 Feb 2020 00:12:37 +0800 Subject: [PATCH 01/39] Refactor constants (#2) - Move ASSETS_HOST to within constants.js - Add a backend directory to facilitate future communication with the backend - Add ability to toggle asset hosts --- .../academy/game/backend/backend.js | 7 +++ .../academy/game/constants/constants.js | 53 ++++++++++--------- .../academy/game/create-initializer.js | 3 +- src/components/academy/game/game.js | 2 - 4 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 src/components/academy/game/backend/backend.js diff --git a/src/components/academy/game/backend/backend.js b/src/components/academy/game/backend/backend.js new file mode 100644 index 0000000000..426eb5619c --- /dev/null +++ b/src/components/academy/game/backend/backend.js @@ -0,0 +1,7 @@ +const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; + +// placeholder URL +const TEST_ASSETS_HOST = 'https://localhost:8080/source-academy-assets/'; + +// placeholder predicate +export const ASSETS_HOST = true ? LIVE_ASSETS_HOST : TEST_ASSETS_HOST; \ No newline at end of file diff --git a/src/components/academy/game/constants/constants.js b/src/components/academy/game/constants/constants.js index c9451ca822..d42c4ca6bb 100644 --- a/src/components/academy/game/constants/constants.js +++ b/src/components/academy/game/constants/constants.js @@ -1,26 +1,27 @@ -module.exports = { - screenWidth: 1920, - screenHeight: 1080, - dialogBoxHeight: 260, - dialogBoxWidth: 1720, - dialogBoxPadding: 25, - fontSize: 44, - innerDialogPadding: 46, - avatarOffset: 46, - nameBoxXPadding: 50, - nameBoxHeight: 80, - playerAvatarSize: 300, - playerAvatarLineWidth: 10, - playerAvatarOffset: 40, - glowDistance: 30, - textSpeed: 0.02, - storyXMLPath: ASSETS_HOST + 'stories/', - locationPath: ASSETS_HOST + 'locations/', - objectPath: ASSETS_HOST + 'objects/', - imgPath: ASSETS_HOST + 'images/', - avatarPath: ASSETS_HOST + 'avatars/', - uiPath: ASSETS_HOST + 'UI/', - soundPath: ASSETS_HOST + 'sounds/', - fadeTime: 0.3, - nullFunction: function() {} -}; +import {ASSETS_HOST} from '../backend/backend' + +export const + screenWidth = 1920, + screenHeight = 1080, + dialogBoxHeight = 260, + dialogBoxWidth = 1720, + dialogBoxPadding = 25, + fontSize = 44, + innerDialogPadding = 46, + avatarOffset = 46, + nameBoxXPadding = 50, + nameBoxHeight = 80, + playerAvatarSize = 300, + playerAvatarLineWidth = 10, + playerAvatarOffset = 40, + glowDistance = 30, + textSpeed = 0.02, + storyXMLPath = ASSETS_HOST + 'stories/', + locationPath = ASSETS_HOST + 'locations/', + objectPath = ASSETS_HOST + 'objects/', + imgPath = ASSETS_HOST + 'images/', + avatarPath = ASSETS_HOST + 'avatars/', + uiPath = ASSETS_HOST + 'UI/', + soundPath = ASSETS_HOST + 'sounds/', + fadeTime = 0.3, + nullFunction = function() {}; \ No newline at end of file diff --git a/src/components/academy/game/create-initializer.js b/src/components/academy/game/create-initializer.js index 5e7a32208c..a641ffc611 100644 --- a/src/components/academy/game/create-initializer.js +++ b/src/components/academy/game/create-initializer.js @@ -1,5 +1,6 @@ import {LINKS} from '../../../utils/constants' import {history} from '../../../utils/history' +import {soundPath} from './constants/constants' export default function (StoryXMLPlayer, story, username, attemptedAll) { function saveToServer() { @@ -45,7 +46,7 @@ export default function (StoryXMLPlayer, story, username, attemptedAll) { } }, playSound: function (name) { - var sound = new Audio(ASSETS_HOST + 'sounds/' + name + '.mp3'); + var sound = new Audio(soundPath + name + '.mp3'); if (sound) { sound.play(); } diff --git a/src/components/academy/game/game.js b/src/components/academy/game/game.js index 63b228f8cd..7dafbe772f 100644 --- a/src/components/academy/game/game.js +++ b/src/components/academy/game/game.js @@ -1,8 +1,6 @@ import createInitializer from './create-initializer' export default function(div, canvas, username, story, attemptedAll) { - window.ASSETS_HOST = - 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; var StoryXMLPlayer = require('./story-xml-player'); var container = document.getElementById('game-display') var initialize = createInitializer(StoryXMLPlayer, story, username, attemptedAll) From 45322d006b24f9cadffe9a3a5c114254aaced0f5 Mon Sep 17 00:00:00 2001 From: wltan <53135010+wltan@users.noreply.github.com> Date: Tue, 25 Feb 2020 16:02:58 +0800 Subject: [PATCH 02/39] Setup game state fetcher Includes implementation to manage active stories --- .../academy/game/backend/game-state.js | 86 +++++++++++++++++++ .../academy/game/create-initializer.js | 5 +- 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 src/components/academy/game/backend/game-state.js diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js new file mode 100644 index 0000000000..4e5b3e7eb4 --- /dev/null +++ b/src/components/academy/game/backend/game-state.js @@ -0,0 +1,86 @@ +import {storyXMLPath} from '../constants/constants' + +/** + * Handles data regarding the game state. + * - The student's list of completed quests + * - The student's current story mission + * - The global list of missions that are open + */ + +let fetched = false; +export function fetchGameData(callback) { + // fetch only needs to be called once; if there are additional calls somehow then ignore them + if(fetched) { + callback(); + return; + } + fetched = true; + const toFetch = [ + fetchCompletedQuests, + fetchStudentMissionPointer, + fetchGlobalMissionPointer + ]; + let remaining = toFetch.length; + // does nothing until the last fetch is completed + const innerCallback = () => (--remaining === 0) ? callback() : undefined; + toFetch.map(x => x(innerCallback)); +} + +function fetchCompletedQuests(callback) { + // placeholder + callback(); +} + +export function getCompletedQuests() { + // placeholder + return null; +} + +function fetchStudentMissionPointer(callback) { + // placeholder + callback(); +} + +function getStudentMissionPointer() { + // placeholder + return 10; +} + +let stories = []; + +function fetchGlobalMissionPointer(callback) { + $.ajax({ + type: 'GET', + url: storyXMLPath + 'master.xml', + dataType: 'xml', + success: xml => { + stories = Array.from(xml.children[0].children); + stories = stories.sort((a, b) => parseInt(a.getAttribute("key")) - parseInt(b.getAttribute("key"))); + }, + error: () => { + loadingOverlay.visible = false; + console.error('Cannot find master story list'); + } + }).then(() => { + const now = new Date(); + const openStory = story => new Date(story.getAttribute("startDate")) < now && now < new Date(story.getAttribute("endDate")); + stories = stories.filter(openStory); + callback(); + }); +} + +/** + * Obtain the story mission to load. This will usually be the student's current mission pointer. + * However, in the event the student's current mission pointer falls outside the bounds of the + * global list of open missions, then the corresponding upper (or lower) bound will be used. + */ +export function getMissionPointer() { + let student = getStudentMissionPointer(); + const newest = parseInt(stories[stories.length-1].getAttribute("key")); // the newest mission to open + const oldest = parseInt(stories[0].getAttribute("key")); // the oldest mission to open + student = Math.min(student, newest); + student = Math.max(student, oldest); + const storyToLoad = stories.filter(story => story.getAttribute("key") == student)[0]; + console.log("Now loading story " + storyToLoad.getAttribute("id")); // debug statement + return storyToLoad.getAttribute("id"); +} \ No newline at end of file diff --git a/src/components/academy/game/create-initializer.js b/src/components/academy/game/create-initializer.js index a641ffc611..5715d3342f 100644 --- a/src/components/academy/game/create-initializer.js +++ b/src/components/academy/game/create-initializer.js @@ -1,6 +1,7 @@ import {LINKS} from '../../../utils/constants' import {history} from '../../../utils/history' import {soundPath} from './constants/constants' +import {fetchGameData, getMissionPointer} from './backend/game-state' export default function (StoryXMLPlayer, story, username, attemptedAll) { function saveToServer() { @@ -81,8 +82,8 @@ export default function (StoryXMLPlayer, story, username, attemptedAll) { } function initialize(div, canvas) { - startGame(div, canvas); - StoryXMLPlayer.loadStory('master', function () {}); + startGame(div, canvas, getStudentData()); + StoryXMLPlayer.loadStory(getMissionPointer(), function () {}); } return initialize; From 87b57d38185161c792d924200967f872dd427270 Mon Sep 17 00:00:00 2001 From: wltan <53135010+wltan@users.noreply.github.com> Date: Tue, 3 Mar 2020 20:48:24 +0800 Subject: [PATCH 03/39] Refactor save and load data --- .../academy/game/backend/backend.js | 7 ----- .../academy/game/backend/game-state.js | 29 +++++++++++++++---- .../academy/game/backend/hosting.js | 9 ++++++ .../academy/game/constants/constants.js | 4 +-- .../academy/game/create-initializer.js | 24 ++++----------- .../academy/game/save-manager/save-manager.js | 9 +++--- .../academy/game/story-xml-player.js | 4 +-- 7 files changed, 47 insertions(+), 39 deletions(-) delete mode 100644 src/components/academy/game/backend/backend.js create mode 100644 src/components/academy/game/backend/hosting.js diff --git a/src/components/academy/game/backend/backend.js b/src/components/academy/game/backend/backend.js deleted file mode 100644 index 426eb5619c..0000000000 --- a/src/components/academy/game/backend/backend.js +++ /dev/null @@ -1,7 +0,0 @@ -const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; - -// placeholder URL -const TEST_ASSETS_HOST = 'https://localhost:8080/source-academy-assets/'; - -// placeholder predicate -export const ASSETS_HOST = true ? LIVE_ASSETS_HOST : TEST_ASSETS_HOST; \ No newline at end of file diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 4e5b3e7eb4..7bec586561 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -2,7 +2,7 @@ import {storyXMLPath} from '../constants/constants' /** * Handles data regarding the game state. - * - The student's list of completed quests + * - The student's list of completed quests and collectibles * - The student's current story mission * - The global list of missions that are open */ @@ -16,7 +16,7 @@ export function fetchGameData(callback) { } fetched = true; const toFetch = [ - fetchCompletedQuests, + fetchStudentData, fetchStudentMissionPointer, fetchGlobalMissionPointer ]; @@ -26,16 +26,35 @@ export function fetchGameData(callback) { toFetch.map(x => x(innerCallback)); } -function fetchCompletedQuests(callback) { +function fetchStudentData(callback) { // placeholder callback(); } -export function getCompletedQuests() { - // placeholder +export function getStudentData() { + // formerly create-initializer/loadFromServer return null; } +export function saveStudentData(json) { + // formerly create-initializer/saveToServer + return json; +} + +export function saveCollectible(collectible) { + // currently local but we should eventually migrate to backend + if (typeof Storage !== 'undefined') { + localStorage.setItem(collectible, 'collected'); + } +} + +export function saveQuest(questId) { + // currently local but we should eventually migrate to backend + if (typeof Storage !== 'undefined') { + localStorage.setItem(questId, 'completed'); + } +} + function fetchStudentMissionPointer(callback) { // placeholder callback(); diff --git a/src/components/academy/game/backend/hosting.js b/src/components/academy/game/backend/hosting.js new file mode 100644 index 0000000000..8ee5cdedeb --- /dev/null +++ b/src/components/academy/game/backend/hosting.js @@ -0,0 +1,9 @@ +const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; + +// placeholder URL +// const TEST_ASSETS_HOST = 'https://localhost:8080/source-academy-assets/'; +const TEST_ASSETS_HOST = 'https://sa2021assets.blob.core.windows.net/sa2021-assets/'; + +// placeholder predicate +export const ASSETS_HOST = LIVE_ASSETS_HOST; +export const STORY_HOST = false ? LIVE_ASSETS_HOST : TEST_ASSETS_HOST; \ No newline at end of file diff --git a/src/components/academy/game/constants/constants.js b/src/components/academy/game/constants/constants.js index d42c4ca6bb..0afaca2514 100644 --- a/src/components/academy/game/constants/constants.js +++ b/src/components/academy/game/constants/constants.js @@ -1,4 +1,4 @@ -import {ASSETS_HOST} from '../backend/backend' +import {ASSETS_HOST, STORY_HOST} from '../backend/hosting' export const screenWidth = 1920, @@ -16,7 +16,7 @@ export const playerAvatarOffset = 40, glowDistance = 30, textSpeed = 0.02, - storyXMLPath = ASSETS_HOST + 'stories/', + storyXMLPath = STORY_HOST + 'stories/', locationPath = ASSETS_HOST + 'locations/', objectPath = ASSETS_HOST + 'objects/', imgPath = ASSETS_HOST + 'images/', diff --git a/src/components/academy/game/create-initializer.js b/src/components/academy/game/create-initializer.js index 5715d3342f..8a41255c0b 100644 --- a/src/components/academy/game/create-initializer.js +++ b/src/components/academy/game/create-initializer.js @@ -1,14 +1,9 @@ import {LINKS} from '../../../utils/constants' import {history} from '../../../utils/history' import {soundPath} from './constants/constants' -import {fetchGameData, getMissionPointer} from './backend/game-state' +import {fetchGameData, getMissionPointer, getStudentData, saveCollectible, saveQuest, saveStudentData} from './backend/game-state' export default function (StoryXMLPlayer, story, username, attemptedAll) { - function saveToServer() { - } - - function loadFromServer() { - } var hookHandlers = { startMission: function () { @@ -41,22 +36,14 @@ export default function (StoryXMLPlayer, story, username, attemptedAll) { return window.open(LINKS.LUMINUS); } }, - pickUpCollectible: function (collectible) { - if (typeof Storage !== 'undefined') { - localStorage.setItem(collectible, 'collected'); - } - }, + pickUpCollectible: saveCollectible, playSound: function (name) { var sound = new Audio(soundPath + name + '.mp3'); if (sound) { sound.play(); } }, - saveCompletedQuest: function (questId) { - if (typeof Storage !== 'undefined') { - localStorage.setItem(questId, 'completed'); - } - } + saveCompletedQuest: saveQuest }; function openWristDevice() { @@ -64,11 +51,10 @@ export default function (StoryXMLPlayer, story, username, attemptedAll) { } function startGame(div, canvas, saveData) { - saveData = saveData || loadFromServer(); + // saveData = saveData || loadFromServer(); StoryXMLPlayer.init(div, canvas, { saveData: saveData, hookHandlers: hookHandlers, - saveFunc: saveToServer, wristDeviceFunc: openWristDevice, playerName: username, playerImageCanvas: $(''), @@ -86,5 +72,5 @@ export default function (StoryXMLPlayer, story, username, attemptedAll) { StoryXMLPlayer.loadStory(getMissionPointer(), function () {}); } - return initialize; + return (div, canvas) => fetchGameData(() => initialize(div, canvas)); }; diff --git a/src/components/academy/game/save-manager/save-manager.js b/src/components/academy/game/save-manager/save-manager.js index 8c93a65c1c..d67813cdb0 100644 --- a/src/components/academy/game/save-manager/save-manager.js +++ b/src/components/academy/game/save-manager/save-manager.js @@ -1,3 +1,5 @@ +import {saveStudentData} from '../backend/game-state'; + var LocationManager = require('../location-manager/location-manager.js'); var QuestManager = require('../quest-manager/quest-manager.js'); var StoryManager = require('../story-manager/story-manager.js'); @@ -7,11 +9,10 @@ var ObjectManager = require('../object-manager/object-manager.js'); var Utils = require('../utils/utils.js'); var actionSequence = []; -var saveFunction; -export function init(saveFunc, saveData, callback) { - saveFunction = saveFunc; +export function init(saveData, callback) { if (saveData) { + alert(saveData); saveData = JSON.parse(saveData); actionSequence = saveData.actionSequence; var storyXMLs = []; @@ -110,7 +111,7 @@ export function saveLoadStories(stories) { } function saveGame() { - saveFunction( + saveStudentData( JSON.stringify({ actionSequence: actionSequence, startLocation: LocationManager.getStartLocation() diff --git a/src/components/academy/game/story-xml-player.js b/src/components/academy/game/story-xml-player.js index 2abe8f5d36..09a080891a 100644 --- a/src/components/academy/game/story-xml-player.js +++ b/src/components/academy/game/story-xml-player.js @@ -18,7 +18,7 @@ var renderer; var stage; //--------LOGIC-------- // options contains the following properties: -// saveData, hookHandlers, saveFunc, wristDeviceFunc +// saveData, hookHandlers, wristDeviceFunc // changeLocationHook, playerImageCanvas, playerName export function init(div, canvas, options, callback) { renderer = PIXI.autoDetectRenderer( @@ -51,7 +51,7 @@ export function init(div, canvas, options, callback) { } animate(); - SaveManager.init(options.saveFunc, options.saveData, callback); + SaveManager.init(options.saveData, callback); // a pixi.container on top of everything that is exported stage.addChild(ExternalManager.init(options.hookHandlers)); From 42fcf7b989f48195dda7472cd26e6db2fd0ed630 Mon Sep 17 00:00:00 2001 From: Asthenosphere Date: Thu, 5 Mar 2020 18:08:16 +0800 Subject: [PATCH 04/39] Create game testing page --- .env.example | 4 +- src/components/academy/NavigationBar.tsx | 11 + src/components/academy/index.tsx | 2 + src/components/game-dev/DeleteCell.tsx | 67 ++++++ src/components/game-dev/DownloadCell.tsx | 43 ++++ src/components/game-dev/Dropzone.tsx | 125 ++++++++++ src/components/game-dev/GameDev.tsx | 33 +++ src/components/game-dev/StoryTable.tsx | 224 ++++++++++++++++++ src/components/game-dev/StoryUpload.tsx | 44 ++++ src/components/game-dev/storyShape.ts | 17 ++ src/containers/game-dev/StoryContainer.ts | 24 ++ .../game-dev/StoryUploadContainer.ts | 38 +++ 12 files changed, 630 insertions(+), 2 deletions(-) create mode 100644 src/components/game-dev/DeleteCell.tsx create mode 100644 src/components/game-dev/DownloadCell.tsx create mode 100644 src/components/game-dev/Dropzone.tsx create mode 100644 src/components/game-dev/GameDev.tsx create mode 100644 src/components/game-dev/StoryTable.tsx create mode 100644 src/components/game-dev/StoryUpload.tsx create mode 100644 src/components/game-dev/storyShape.ts create mode 100644 src/containers/game-dev/StoryContainer.ts create mode 100644 src/containers/game-dev/StoryUploadContainer.ts diff --git a/.env.example b/.env.example index ecfdf7b51c..02c73e6d2f 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -REACT_APP_LUMINUS_CLIENT_ID=your_luminus_client_id_here +REACT_APP_LUMINUS_CLIENT_ID=nus_apps REACT_APP_VERSION=$npm_package_version REACT_APP_BACKEND_URL=http://localhost:4001 REACT_APP_USE_BACKEND=TRUE REACT_APP_CHATKIT_INSTANCE_LOCATOR=instance_locator_here_otherwise_empty_string -MODULE_BACKEND_URL=http://ec2-54-169-81-133.ap-southeast-1.compute.amazonaws.com \ No newline at end of file +MODULE_BACKEND_URL=http://ec2-54-169-81-133.ap-southeast-1.compute.amazonaws.com diff --git a/src/components/academy/NavigationBar.tsx b/src/components/academy/NavigationBar.tsx index cbd9b8b740..2db2dc7185 100644 --- a/src/components/academy/NavigationBar.tsx +++ b/src/components/academy/NavigationBar.tsx @@ -111,6 +111,17 @@ const NavigationBar: React.SFC = props => ( disableHover={true} /> + + + +
Game Dev
+
+ + ) : null} diff --git a/src/components/academy/index.tsx b/src/components/academy/index.tsx index bad17fcd0b..6b37836af1 100644 --- a/src/components/academy/index.tsx +++ b/src/components/academy/index.tsx @@ -3,6 +3,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router'; import Grading from '../../containers/academy/grading'; import AssessmentContainer from '../../containers/assessment'; +import StoryUpload from "../../containers/game-dev/StoryUploadContainer"; import Game from '../../containers/GameContainer'; import MaterialUpload from '../../containers/material/MaterialUploadContainer'; import Sourcereel from '../../containers/sourcecast/SourcereelContainer'; @@ -80,6 +81,7 @@ class Academy extends React.Component { + diff --git a/src/components/game-dev/DeleteCell.tsx b/src/components/game-dev/DeleteCell.tsx new file mode 100644 index 0000000000..a10fea649e --- /dev/null +++ b/src/components/game-dev/DeleteCell.tsx @@ -0,0 +1,67 @@ +import { Classes, Dialog } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; + +import { controlButton } from '../commons'; +import { MaterialData } from './storyShape'; + +interface IDeleteCellProps { + data: MaterialData; + handleDeleteMaterial: (id: number) => void; + handleDeleteMaterialFolder: (id: number) => void; +} + +interface IDeleteCellState { + dialogOpen: boolean; +} + +class DeleteCell extends React.Component { + public constructor(props: IDeleteCellProps) { + super(props); + this.state = { + dialogOpen: false + }; + } + + public render() { + return ( +
+ {controlButton('', IconNames.TRASH, this.handleOpenDialog)} + +
+ {this.props.data.url ? ( +

Are you sure to delete this material file?

+ ) : ( +

Are you sure to delete this material folder?

+ )} +
+
+
+ {controlButton('Confirm Delete', IconNames.TRASH, this.handleDelete)} + {controlButton('Cancel', IconNames.CROSS, this.handleCloseDialog)} +
+
+
+
+ ); + } + + private handleCloseDialog = () => this.setState({ dialogOpen: false }); + private handleOpenDialog = () => this.setState({ dialogOpen: true }); + private handleDelete = () => { + const { data } = this.props; + if (data.url) { + this.props.handleDeleteMaterial(data.id); + } else { + this.props.handleDeleteMaterialFolder(data.id); + } + }; +} + +export default DeleteCell; diff --git a/src/components/game-dev/DownloadCell.tsx b/src/components/game-dev/DownloadCell.tsx new file mode 100644 index 0000000000..cd400a804f --- /dev/null +++ b/src/components/game-dev/DownloadCell.tsx @@ -0,0 +1,43 @@ +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; + +import { controlButton } from '../commons'; +import { MaterialData } from './storyShape'; + +interface ISelectCellProps { + data: MaterialData; + handleFetchMaterialIndex: (id?: number) => void; +} + +class DownloadCell extends React.Component { + public constructor(props: ISelectCellProps) { + super(props); + } + + public render() { + return ( +
+ {this.props.data.url + ? controlButton(`${this.props.data.title}`, null, this.handleDownload) + : controlButton(`${this.props.data.title}`, IconNames.FOLDER_CLOSE, this.handleSelect)} +
+ ); + } + + private handleDownload = () => { + const url = this.props.data.url; + const click = document.createEvent('Event'); + click.initEvent('click', true, true); + const link = document.createElement('A') as HTMLAnchorElement; + link.href = url; + link.dispatchEvent(click); + link.click(); + return link; + }; + + private handleSelect = () => { + this.props.handleFetchMaterialIndex(this.props.data.id); + }; +} + +export default DownloadCell; diff --git a/src/components/game-dev/Dropzone.tsx b/src/components/game-dev/Dropzone.tsx new file mode 100644 index 0000000000..c445e2e3e6 --- /dev/null +++ b/src/components/game-dev/Dropzone.tsx @@ -0,0 +1,125 @@ +import { Card, EditableText, Elevation } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { FlexDirectionProperty } from 'csstype'; +import * as React from 'react'; +import { useDropzone } from 'react-dropzone'; + +import { controlButton } from '../commons'; + +interface IDropzoneType { + handleUploadMaterial: (file: File, title: string, description: string) => void; +} + +// Dropzone styling +const dropZoneStyle = { + baseStyle: { + flex: 1, + display: 'flex', + height: '30vh', + flexDirection: 'column' as FlexDirectionProperty, + alignItems: 'center', + justifyContent: 'center', + padding: '20px', + borderWidth: 2, + borderRadius: 2, + borderColor: '#eeeeee', + borderStyle: 'dashed', + backgroundColor: '#fafafa', + color: '#bdbdbd', + outline: 'none', + transition: 'border .24s ease-in-out' + }, + + activeStyle: { + borderColor: '#2196f3' + }, + + acceptStyle: { + borderColor: '#00e676' + }, + + rejectStyle: { + borderColor: '#ff1744' + } +}; + +const MaterialDropzone: React.FC = props => { + const [file, setFile] = React.useState(); + const [title, setTitle] = React.useState(); + const [description, setDescription] = React.useState(''); + const handleSetTitle = (value: string) => setTitle(value); + const handleSetDescription = (value: string) => setDescription(value); + const handleConfirmUpload = () => { + props.handleUploadMaterial(file!, title!, description); + setFile(undefined); + }; + const handleCancelUpload = () => setFile(undefined); + + const { + getRootProps, + getInputProps, + isDragActive, + isDragAccept, + isDragReject, + isFocused + } = useDropzone({ + onDrop: acceptedFiles => { + setFile(acceptedFiles[0]); + setTitle(acceptedFiles[0].name); + } + }); + const style = React.useMemo( + () => ({ + ...dropZoneStyle.baseStyle, + ...(isDragActive ? dropZoneStyle.activeStyle : {}), + ...(isDragAccept ? dropZoneStyle.acceptStyle : {}), + ...(isDragReject ? dropZoneStyle.rejectStyle : {}), + ...(isFocused ? dropZoneStyle.activeStyle : {}) + }), + [isDragActive, isDragAccept, isDragReject, isFocused] + ); + + return ( + <> + +
+ +

Drag 'n' drop some files here, or click to select files

+
+
+ {file && ( + + +
+ +
+ {controlButton('Confirm Upload', IconNames.UPLOAD, handleConfirmUpload)} + {controlButton('Cancel Upload', IconNames.DELETE, handleCancelUpload)} +
+ )} + + ); +}; + +export default MaterialDropzone; diff --git a/src/components/game-dev/GameDev.tsx b/src/components/game-dev/GameDev.tsx new file mode 100644 index 0000000000..8409be2347 --- /dev/null +++ b/src/components/game-dev/GameDev.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import { DirectoryData, MaterialData } from './storyShape'; +import StoryTable from './StoryTable'; + +interface IMaterialProps extends IDispatchProps, IStateProps {} + +export interface IDispatchProps { + handleFetchMaterialIndex: (id?: number) => void; +} + +export interface IStateProps { + materialDirectoryTree: DirectoryData[] | null; + materialIndex: MaterialData[] | null; +} + +class GameDev extends React.Component { + public render() { + return ( +
+
+ +
+
+ ); + } +} + +export default GameDev; diff --git a/src/components/game-dev/StoryTable.tsx b/src/components/game-dev/StoryTable.tsx new file mode 100644 index 0000000000..7de5a534ca --- /dev/null +++ b/src/components/game-dev/StoryTable.tsx @@ -0,0 +1,224 @@ +import { + Button, + Card, + Classes, + Divider, + Elevation, + FormGroup, + InputGroup, + NonIdealState, + OverflowList, + Spinner +} from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { ColDef, GridApi, GridReadyEvent } from 'ag-grid'; +import { AgGridReact } from 'ag-grid-react'; +import 'ag-grid/dist/styles/ag-grid.css'; +import 'ag-grid/dist/styles/ag-theme-material.css'; +import * as classNames from 'classnames'; +import { sortBy } from 'lodash'; +import * as React from 'react'; + +import { getStandardDateTime } from '../../utils/dateHelpers'; +import { controlButton } from '../commons'; +import DeleteCell from './DeleteCell'; +import DownloadCell from './DownloadCell'; +import { DirectoryData, MaterialData } from './storyShape'; + +/** + * Column Definitions are defined within the state, so that data + * can be manipulated easier. See constructor for an example. + */ +type State = { + columnDefs: ColDef[]; + dialogOpen: boolean; + filterValue: string; + groupFilterEnabled: boolean; + newFolderName: string; +}; + +type IMaterialTableProps = IOwnProps; + +interface IOwnProps { + handleCreateMaterialFolder?: (title: string) => void; + handleDeleteMaterial?: (id: number) => void; + handleDeleteMaterialFolder?: (id: number) => void; + handleFetchMaterialIndex: (id?: number) => void; + materialDirectoryTree: DirectoryData[] | null; + materialIndex: MaterialData[] | null; +} + +class StoryTable extends React.Component { + private gridApi?: GridApi; + + public constructor(props: IMaterialTableProps) { + super(props); + + this.state = { + columnDefs: [ + { + headerName: 'Title', + field: 'title', + cellRendererFramework: DownloadCell, + cellRendererParams: { + handleFetchMaterialIndex: this.props.handleFetchMaterialIndex + }, + width: 800, + suppressMovable: true, + suppressMenu: true, + autoHeight: true, + cellStyle: { + 'text-align': 'left' + } + }, + { + headerName: 'Uploader', + field: 'uploader.name', + width: 400, + suppressMovable: true, + suppressMenu: true + }, + { + headerName: 'Date', + valueGetter: params => getStandardDateTime(params.data.inserted_at), + width: 400, + suppressMovable: true, + suppressMenu: true + }, + { + headerName: 'Delete', + field: '', + cellRendererFramework: DeleteCell, + cellRendererParams: { + handleDeleteMaterial: this.props.handleDeleteMaterial, + handleDeleteMaterialFolder: this.props.handleDeleteMaterialFolder + }, + width: 150, + suppressSorting: true, + suppressMovable: true, + suppressMenu: true, + cellStyle: { + padding: 0 + }, + hide: !this.props.handleDeleteMaterial + }, + { headerName: 'description', field: 'description', hide: true }, + { headerName: 'inserted_at', field: 'inserted_at', hide: true }, + { headerName: 'updated_at', field: 'updated_at', hide: true }, + { headerName: 'url', field: 'url', hide: true } + ], + dialogOpen: false, + filterValue: '', + groupFilterEnabled: false, + newFolderName: '' + }; + } + + public componentDidMount() { + this.props.handleFetchMaterialIndex(); + } + + public render() { + /* Display either a loading screen or a table with overviews. */ + const loadingDisplay = ( + } + /> + ); + const data = sortBy(this.props.materialIndex, [(a: any) => -a.url]); + const grid = ( +
+
+

Story XML files

+ + +
+ +
+
+
+ +
+
+ +
+
+ + {" "} + +
+ ); + return ( + + {this.props.materialIndex === undefined ? loadingDisplay : grid} + + ); + } + + private renderBreadcrumb = (data: DirectoryData, index: number) => { + return ( + + {controlButton(`${data.title}`, IconNames.CHEVRON_RIGHT, () => + this.props.handleFetchMaterialIndex(data.id) + )} + + ); + }; + + private handleFilterChange = (event: React.ChangeEvent) => { + const changeVal = event.target.value; + this.setState({ filterValue: changeVal }); + + if (this.gridApi) { + this.gridApi.setQuickFilter(changeVal); + } + }; + + private handleTest = () => { + window.open("/academy/game"); + }; + + private handleReset = () => { + window.alert("Are you sure you want to discard all changes made?"); + }; + + private onGridReady = (params: GridReadyEvent) => { + this.gridApi = params.api; + this.gridApi.sizeColumnsToFit(); + window.onresize = () => this.gridApi!.sizeColumnsToFit(); + }; +} + +export default StoryTable; diff --git a/src/components/game-dev/StoryUpload.tsx b/src/components/game-dev/StoryUpload.tsx new file mode 100644 index 0000000000..059f01deef --- /dev/null +++ b/src/components/game-dev/StoryUpload.tsx @@ -0,0 +1,44 @@ +import { Divider } from '@blueprintjs/core'; +import * as React from 'react'; + +import Dropzone from './Dropzone'; +import { DirectoryData, MaterialData } from './storyShape'; +import StoryTable from './StoryTable'; + +interface IStoryProps extends IDispatchProps, IStateProps {} + +export interface IDispatchProps { + handleCreateMaterialFolder: (title: string) => void; + handleDeleteMaterial: (id: number) => void; + handleDeleteMaterialFolder: (id: number) => void; + handleFetchMaterialIndex: (id?: number) => void; + handleUploadMaterial: (file: File, title: string, description: string) => void; +} + +export interface IStateProps { + materialDirectoryTree: DirectoryData[] | null; + materialIndex: MaterialData[] | null; +} + +class StoryUpload extends React.Component { + public render() { + return ( +
+
+ + + +
+
+ ); + } +} + +export default StoryUpload; diff --git a/src/components/game-dev/storyShape.ts b/src/components/game-dev/storyShape.ts new file mode 100644 index 0000000000..63ff9db4e3 --- /dev/null +++ b/src/components/game-dev/storyShape.ts @@ -0,0 +1,17 @@ +export type MaterialData = { + title: string; + description: string; + inserted_at: string; + updated_at: string; + id: number; + uploader: { + id: number; + name: string; + }; + url: string; +}; + +export type DirectoryData = { + id: number; + title: string; +}; diff --git a/src/containers/game-dev/StoryContainer.ts b/src/containers/game-dev/StoryContainer.ts new file mode 100644 index 0000000000..79899e7e24 --- /dev/null +++ b/src/containers/game-dev/StoryContainer.ts @@ -0,0 +1,24 @@ +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { fetchMaterialIndex } from '../../actions'; +import GameDev, { IDispatchProps, IStateProps } from '../../components/game-dev/GameDev'; +import { IState } from '../../reducers/states'; + +const mapStateToProps: MapStateToProps = state => ({ + materialDirectoryTree: state.session.materialDirectoryTree, + materialIndex: state.session.materialIndex +}); + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + handleFetchMaterialIndex: (id?: number) => fetchMaterialIndex(id) + }, + dispatch + ); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(GameDev); diff --git a/src/containers/game-dev/StoryUploadContainer.ts b/src/containers/game-dev/StoryUploadContainer.ts new file mode 100644 index 0000000000..6a32275691 --- /dev/null +++ b/src/containers/game-dev/StoryUploadContainer.ts @@ -0,0 +1,38 @@ +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { + // createMaterialFolder, + deleteMaterial, + deleteMaterialFolder, + fetchMaterialIndex, + uploadMaterial +} from '../../actions'; +import StoryUpload, { + IDispatchProps, + IStateProps +} from '../../components/game-dev/StoryUpload'; +import { IState } from '../../reducers/states'; + +const mapStateToProps: MapStateToProps = state => ({ + materialDirectoryTree: state.session.materialDirectoryTree, + materialIndex: state.session.materialIndex +}); + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + handleCreateMaterialFolder: (title: string) => null, + handleDeleteMaterial: (id: number) => deleteMaterial(id), + handleDeleteMaterialFolder: (id: number) => deleteMaterialFolder(id), + handleFetchMaterialIndex: (id?: number) => fetchMaterialIndex(id), + handleUploadMaterial: (file: File, title: string, description: string) => + uploadMaterial(file, title, description) + }, + dispatch + ); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(StoryUpload); From 84926a2b6d83cfeb7873698daf9dd233fcf61637 Mon Sep 17 00:00:00 2001 From: Asthenosphere Date: Thu, 5 Mar 2020 18:19:36 +0800 Subject: [PATCH 05/39] Create telescope/night filter --- .../game/filter-effects/filter-effects.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/academy/game/filter-effects/filter-effects.js b/src/components/academy/game/filter-effects/filter-effects.js index ed2b2d6cf4..e296f67c5f 100644 --- a/src/components/academy/game/filter-effects/filter-effects.js +++ b/src/components/academy/game/filter-effects/filter-effects.js @@ -37,6 +37,29 @@ export function createGlowTexture(displayObject) { return createTexture(container, [glowFilter], width, height); } +export function createTelescopeEffect(parent) { + const background = parent; + const radius = 300; + const blurSize = 16; + const circle = new PIXI.Graphics() + .beginFill(0xff0000) + .drawCircle(radius + blurSize, radius + blurSize, radius) + .endFill(); + circle.filters = [new PIXI.filters.BlurFilter(blurSize)]; + const bounds = new PIXI.Rectangle(0, 0, (radius + blurSize) * 2, (radius + blurSize) * 2); + const renderer = getRenderer(); + const texture = renderer.generateTexture(circle, PIXI.SCALE_MODES.NEAREST, 1, bounds); + const focus = new PIXI.Sprite(texture); + parent.addChild(focus); + background.mask = focus; + parent.interactive = true; + parent.on('mousemove', pointerMove); + function pointerMove(event) { + focus.position.x = event.data.global.x - focus.width / 2; + focus.position.y = event.data.global.y - focus.height / 2; + } +} + export function createDarkenedTexture(texture) { return createFilteredTexture(texture, [darkFilter]); } From c50f391b293f40cd2842db94f9858c007a1b4fe3 Mon Sep 17 00:00:00 2001 From: Asthenosphere Date: Fri, 20 Mar 2020 15:10:18 +0800 Subject: [PATCH 06/39] Add test for Game Dev link --- .../__tests__/__snapshots__/NavigationBar.tsx.snap | 12 ++++++++++++ .../academy/game/filter-effects/filter-effects.js | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap index 7538039059..b4f4207862 100644 --- a/src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -99,6 +99,12 @@ exports[`Grading NavLink renders for Role.Admin 1`] = ` + + +
+ Game Dev +
+
" `; @@ -161,6 +167,12 @@ exports[`Grading NavLink renders for Role.Staff 1`] = ` + + +
+ Game Dev +
+
" `; diff --git a/src/components/academy/game/filter-effects/filter-effects.js b/src/components/academy/game/filter-effects/filter-effects.js index e296f67c5f..011e69d22a 100644 --- a/src/components/academy/game/filter-effects/filter-effects.js +++ b/src/components/academy/game/filter-effects/filter-effects.js @@ -60,6 +60,8 @@ export function createTelescopeEffect(parent) { } } + + export function createDarkenedTexture(texture) { return createFilteredTexture(texture, [darkFilter]); } From 670786fe38ea8cdf99441bfb70bd0e9cbdc7d017 Mon Sep 17 00:00:00 2001 From: wltan Date: Thu, 5 Mar 2020 13:52:07 +0800 Subject: [PATCH 07/39] Add override functions --- src/components/academy/game/backend/game-state.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 7bec586561..d39fafc5d4 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -26,6 +26,17 @@ export function fetchGameData(callback) { toFetch.map(x => x(innerCallback)); } +// overrides +let studentDataOverride = undefined, + missionPointerOverride = undefined, + currentDateOverride = undefined; +// override student game data +export function overrideStudentData(data) { studentDataOverride = data; } +// override student's current mission +export function overrideMissionPointer(data) { missionPointerOverride = data; } +// override current date (to determine active missions) +export function overrideCurrentDate(data) { currentDateOverride = data; } + function fetchStudentData(callback) { // placeholder callback(); @@ -33,6 +44,7 @@ function fetchStudentData(callback) { export function getStudentData() { // formerly create-initializer/loadFromServer + if(studentDataOverride) return studentDataOverride; return null; } @@ -62,6 +74,7 @@ function fetchStudentMissionPointer(callback) { function getStudentMissionPointer() { // placeholder + if(missionPointerOverride) return missionPointerOverride; return 10; } @@ -81,7 +94,7 @@ function fetchGlobalMissionPointer(callback) { console.error('Cannot find master story list'); } }).then(() => { - const now = new Date(); + const now = currentDateOverride ? currentDateOverride : new Date(); const openStory = story => new Date(story.getAttribute("startDate")) < now && now < new Date(story.getAttribute("endDate")); stories = stories.filter(openStory); callback(); From 3c299962342dc044f812f47625fd63e998c287e5 Mon Sep 17 00:00:00 2001 From: wltan Date: Fri, 27 Mar 2020 16:11:33 +0800 Subject: [PATCH 08/39] Retrieve data from backend --- .../academy/game/backend/game-state.js | 33 +++++-------------- .../academy/game/backend/hosting.js | 4 ++- src/components/academy/game/backend/user.js | 8 +++++ .../academy/game/create-initializer.js | 5 ++- src/components/academy/game/game.js | 4 +-- src/components/academy/game/index.tsx | 27 ++++----------- 6 files changed, 30 insertions(+), 51 deletions(-) create mode 100644 src/components/academy/game/backend/user.js diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index d39fafc5d4..30380594f6 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -6,24 +6,20 @@ import {storyXMLPath} from '../constants/constants' * - The student's current story mission * - The global list of missions that are open */ - let fetched = false; -export function fetchGameData(callback) { +let studentMissionPointer = undefined, + studentData = undefined; +export function fetchGameData(userStory, callback) { // fetch only needs to be called once; if there are additional calls somehow then ignore them if(fetched) { callback(); return; } fetched = true; - const toFetch = [ - fetchStudentData, - fetchStudentMissionPointer, - fetchGlobalMissionPointer - ]; - let remaining = toFetch.length; - // does nothing until the last fetch is completed - const innerCallback = () => (--remaining === 0) ? callback() : undefined; - toFetch.map(x => x(innerCallback)); + studentMissionPointer = userStory.story; + // not implemented yet + studentData = undefined; // userStory.data; + fetchGlobalMissionPointer(callback); } // overrides @@ -37,15 +33,10 @@ export function overrideMissionPointer(data) { missionPointerOverride = data; } // override current date (to determine active missions) export function overrideCurrentDate(data) { currentDateOverride = data; } -function fetchStudentData(callback) { - // placeholder - callback(); -} - export function getStudentData() { // formerly create-initializer/loadFromServer if(studentDataOverride) return studentDataOverride; - return null; + return studentData; } export function saveStudentData(json) { @@ -67,15 +58,10 @@ export function saveQuest(questId) { } } -function fetchStudentMissionPointer(callback) { - // placeholder - callback(); -} - function getStudentMissionPointer() { // placeholder if(missionPointerOverride) return missionPointerOverride; - return 10; + return studentMissionPointer; } let stories = []; @@ -90,7 +76,6 @@ function fetchGlobalMissionPointer(callback) { stories = stories.sort((a, b) => parseInt(a.getAttribute("key")) - parseInt(b.getAttribute("key"))); }, error: () => { - loadingOverlay.visible = false; console.error('Cannot find master story list'); } }).then(() => { diff --git a/src/components/academy/game/backend/hosting.js b/src/components/academy/game/backend/hosting.js index 8ee5cdedeb..e18a7a1286 100644 --- a/src/components/academy/game/backend/hosting.js +++ b/src/components/academy/game/backend/hosting.js @@ -1,3 +1,5 @@ +import { isStudent } from './user'; + const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; // placeholder URL @@ -6,4 +8,4 @@ const TEST_ASSETS_HOST = 'https://sa2021assets.blob.core.windows.net/sa2021-asse // placeholder predicate export const ASSETS_HOST = LIVE_ASSETS_HOST; -export const STORY_HOST = false ? LIVE_ASSETS_HOST : TEST_ASSETS_HOST; \ No newline at end of file +export const STORY_HOST = isStudent() ? LIVE_ASSETS_HOST : TEST_ASSETS_HOST; \ No newline at end of file diff --git a/src/components/academy/game/backend/user.js b/src/components/academy/game/backend/user.js new file mode 100644 index 0000000000..19c404dd12 --- /dev/null +++ b/src/components/academy/game/backend/user.js @@ -0,0 +1,8 @@ +let userRole = undefined; +export function setUserRole(role) { + userRole = role; +} + +export function isStudent() { + return userRole === "student"; +} \ No newline at end of file diff --git a/src/components/academy/game/create-initializer.js b/src/components/academy/game/create-initializer.js index 8a41255c0b..6234fef762 100644 --- a/src/components/academy/game/create-initializer.js +++ b/src/components/academy/game/create-initializer.js @@ -3,7 +3,7 @@ import {history} from '../../../utils/history' import {soundPath} from './constants/constants' import {fetchGameData, getMissionPointer, getStudentData, saveCollectible, saveQuest, saveStudentData} from './backend/game-state' -export default function (StoryXMLPlayer, story, username, attemptedAll) { +export default function (StoryXMLPlayer, username, userStory) { var hookHandlers = { startMission: function () { @@ -51,7 +51,6 @@ export default function (StoryXMLPlayer, story, username, attemptedAll) { } function startGame(div, canvas, saveData) { - // saveData = saveData || loadFromServer(); StoryXMLPlayer.init(div, canvas, { saveData: saveData, hookHandlers: hookHandlers, @@ -72,5 +71,5 @@ export default function (StoryXMLPlayer, story, username, attemptedAll) { StoryXMLPlayer.loadStory(getMissionPointer(), function () {}); } - return (div, canvas) => fetchGameData(() => initialize(div, canvas)); + return (div, canvas) => fetchGameData(userStory, () => initialize(div, canvas)); }; diff --git a/src/components/academy/game/game.js b/src/components/academy/game/game.js index 7dafbe772f..f08ba9bb29 100644 --- a/src/components/academy/game/game.js +++ b/src/components/academy/game/game.js @@ -1,8 +1,8 @@ import createInitializer from './create-initializer' -export default function(div, canvas, username, story, attemptedAll) { +export default function(div, canvas, username, userStory) { var StoryXMLPlayer = require('./story-xml-player'); var container = document.getElementById('game-display') - var initialize = createInitializer(StoryXMLPlayer, story, username, attemptedAll) + var initialize = createInitializer(StoryXMLPlayer, username, userStory) initialize(div, canvas); } diff --git a/src/components/academy/game/index.tsx b/src/components/academy/game/index.tsx index e9d4e14ba0..8b7ba94194 100644 --- a/src/components/academy/game/index.tsx +++ b/src/components/academy/game/index.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { store } from '../../../createStore'; import { Story } from '../../../reducers/states'; +import { setUserRole } from './backend/user'; type GameProps = DispatchProps & StateProps; @@ -39,7 +40,7 @@ export class Game extends React.Component { const story: any = (await import('./game.js')).default; if (this.props.canvas === undefined) { const storyOpts = await this.getStoryOpts(); - story(this.div, this.canvas, this.props.name, ...storyOpts); + story(this.div, this.canvas, this.props.name, storyOpts); this.props.handleSaveCanvas(this.canvas); } else { // This browser window has loaded the Game component & canvas before @@ -57,26 +58,10 @@ export class Game extends React.Component { } private async getStoryOpts() { - if (this.props.story) { - // no missions, no story from backend, just play intro - return this.props.story.story - ? [this.props.story.story, !this.props.story.playStory] - : ['mission-1', true]; - } else { - // this.props.story is null if creating 'fresh' store from localStorage - const state = store.getState(); - if (state.session.story) { - // no missions, no story from backend, just play intro - return state.session.story.story - ? [state.session.story.story, !state.session.story.playStory] - : ['mission-1', true]; - } else { - // if user is null, actions.logOut is called anyways; nonetheless we - // return a storyOpts, otherwise typescript complains about using storyOpts - // before assignment in story/4 below - return ['mission-1', true]; - } - } + const defaultStory = { story: 10, playStory: true }; + const userStory = this.props.story ? this.props.story : store.getState().session.story; + setUserRole(store.getState().session.role); + return userStory ? userStory : defaultStory; } } From dd3932a37b00468e7590c7e8a2dab886a61fa9f6 Mon Sep 17 00:00:00 2001 From: Asthenosphere Date: Mon, 30 Mar 2020 16:35:36 +0800 Subject: [PATCH 09/39] Add more tests for game-dev page and material page --- src/components/game-dev/GameDev.tsx | 2 +- src/components/game-dev/StoryTable.tsx | 2 +- src/components/game-dev/StoryUpload.tsx | 2 +- src/components/game-dev/__tests__/GameDev.tsx | 15 +++++++ .../game-dev/__tests__/StoryTable.tsx | 22 ++++++++++ .../game-dev/__tests__/StoryUpload.tsx | 19 +++++++++ .../__tests__/__snapshots__/GameDev.tsx.snap | 9 ++++ .../__snapshots__/StoryTable.tsx.snap | 36 ++++++++++++++++ .../__snapshots__/StoryUpload.tsx.snap | 11 +++++ src/components/material/Material.tsx | 2 +- src/components/material/MaterialTable.tsx | 2 +- src/components/material/MaterialUpload.tsx | 2 +- .../material/__tests__/Material.tsx | 15 +++++++ .../material/__tests__/MaterialTable.tsx | 22 ++++++++++ .../material/__tests__/MaterialUpload.tsx | 19 +++++++++ .../__tests__/__snapshots__/Material.tsx.snap | 9 ++++ .../__snapshots__/MaterialTable.tsx.snap | 42 +++++++++++++++++++ .../__snapshots__/MaterialUpload.tsx.snap | 11 +++++ 18 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 src/components/game-dev/__tests__/GameDev.tsx create mode 100644 src/components/game-dev/__tests__/StoryTable.tsx create mode 100644 src/components/game-dev/__tests__/StoryUpload.tsx create mode 100644 src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap create mode 100644 src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap create mode 100644 src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap create mode 100644 src/components/material/__tests__/Material.tsx create mode 100644 src/components/material/__tests__/MaterialTable.tsx create mode 100644 src/components/material/__tests__/MaterialUpload.tsx create mode 100644 src/components/material/__tests__/__snapshots__/Material.tsx.snap create mode 100644 src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap create mode 100644 src/components/material/__tests__/__snapshots__/MaterialUpload.tsx.snap diff --git a/src/components/game-dev/GameDev.tsx b/src/components/game-dev/GameDev.tsx index 8409be2347..ef9d1f8a73 100644 --- a/src/components/game-dev/GameDev.tsx +++ b/src/components/game-dev/GameDev.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { DirectoryData, MaterialData } from './storyShape'; import StoryTable from './StoryTable'; -interface IMaterialProps extends IDispatchProps, IStateProps {} +export interface IMaterialProps extends IDispatchProps, IStateProps {} export interface IDispatchProps { handleFetchMaterialIndex: (id?: number) => void; diff --git a/src/components/game-dev/StoryTable.tsx b/src/components/game-dev/StoryTable.tsx index 7de5a534ca..d8588ba6f9 100644 --- a/src/components/game-dev/StoryTable.tsx +++ b/src/components/game-dev/StoryTable.tsx @@ -39,7 +39,7 @@ type State = { type IMaterialTableProps = IOwnProps; -interface IOwnProps { +export interface IOwnProps { handleCreateMaterialFolder?: (title: string) => void; handleDeleteMaterial?: (id: number) => void; handleDeleteMaterialFolder?: (id: number) => void; diff --git a/src/components/game-dev/StoryUpload.tsx b/src/components/game-dev/StoryUpload.tsx index 059f01deef..0f613341a1 100644 --- a/src/components/game-dev/StoryUpload.tsx +++ b/src/components/game-dev/StoryUpload.tsx @@ -5,7 +5,7 @@ import Dropzone from './Dropzone'; import { DirectoryData, MaterialData } from './storyShape'; import StoryTable from './StoryTable'; -interface IStoryProps extends IDispatchProps, IStateProps {} +export interface IStoryProps extends IDispatchProps, IStateProps {} export interface IDispatchProps { handleCreateMaterialFolder: (title: string) => void; diff --git a/src/components/game-dev/__tests__/GameDev.tsx b/src/components/game-dev/__tests__/GameDev.tsx new file mode 100644 index 0000000000..e10f27dbe0 --- /dev/null +++ b/src/components/game-dev/__tests__/GameDev.tsx @@ -0,0 +1,15 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; + +import GameDev, { IMaterialProps } from "../GameDev"; + +test('GameDev page renders correctly', () => { + const props: IMaterialProps = { + handleFetchMaterialIndex (p1: number) { + }, materialDirectoryTree: null, materialIndex: null + }; + const app = ; + const tree = shallow(app); + expect(tree.debug()).toMatchSnapshot(); +}); + diff --git a/src/components/game-dev/__tests__/StoryTable.tsx b/src/components/game-dev/__tests__/StoryTable.tsx new file mode 100644 index 0000000000..4ae432bbc7 --- /dev/null +++ b/src/components/game-dev/__tests__/StoryTable.tsx @@ -0,0 +1,22 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; + +import StoryTable, { IOwnProps } from "../StoryTable"; + +const componentDidMountSpy = jest.fn(); + +jest.spyOn(StoryTable.prototype, 'componentDidMount').mockImplementation(componentDidMountSpy); + +test('Story table renders correctly', () => { + const props: IOwnProps = { + handleCreateMaterialFolder (p1: string) { + }, handleDeleteMaterial (p1: number) { + }, handleDeleteMaterialFolder (p1: number) { + }, handleFetchMaterialIndex (p1: number) { + }, materialDirectoryTree: null, materialIndex: null + }; + const app = ; + const tree = shallow(app); + expect(tree.debug()).toMatchSnapshot(); + expect(componentDidMountSpy).toHaveBeenCalled(); +}); diff --git a/src/components/game-dev/__tests__/StoryUpload.tsx b/src/components/game-dev/__tests__/StoryUpload.tsx new file mode 100644 index 0000000000..925abbdbbe --- /dev/null +++ b/src/components/game-dev/__tests__/StoryUpload.tsx @@ -0,0 +1,19 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; + +import StoryUpload, { IStoryProps } from "../StoryUpload"; + +test("Story Upload area renders correctly", () => { + const props: IStoryProps = { + handleCreateMaterialFolder (p1: string) { + }, handleDeleteMaterial (p1: number) { + }, handleDeleteMaterialFolder (p1: number) { + }, handleFetchMaterialIndex (p1: number) { + }, handleUploadMaterial (p1: File, p2: string, p3: string) { + }, materialDirectoryTree: null, materialIndex: null + + }; + const app = ; + const tree = shallow(app); + expect(tree.debug()).toMatchSnapshot(); +}); diff --git a/src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap b/src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap new file mode 100644 index 0000000000..5925d11a5e --- /dev/null +++ b/src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GameDev page renders correctly 1`] = ` +"
+
+ +
+
" +`; diff --git a/src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap b/src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap new file mode 100644 index 0000000000..312fc94533 --- /dev/null +++ b/src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Story table renders correctly 1`] = ` +" +
+
+

+ Story XML files +

+ + +
+ +
+
+
+ +
+
+ +
+
+ +
+ Open Game in new Window +
+
+ + +
+ Load XML files from AWS +
+
+
+
" +`; diff --git a/src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap b/src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap new file mode 100644 index 0000000000..775f3703b7 --- /dev/null +++ b/src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Story Upload area renders correctly 1`] = ` +"
+
+ + + +
+
" +`; diff --git a/src/components/material/Material.tsx b/src/components/material/Material.tsx index bf133d0541..1505068ced 100644 --- a/src/components/material/Material.tsx +++ b/src/components/material/Material.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { DirectoryData, MaterialData } from './materialShape'; import MaterialTable from './MaterialTable'; -interface IMaterialProps extends IDispatchProps, IStateProps {} +export interface IMaterialProps extends IDispatchProps, IStateProps {} export interface IDispatchProps { handleFetchMaterialIndex: (id?: number) => void; diff --git a/src/components/material/MaterialTable.tsx b/src/components/material/MaterialTable.tsx index fb3b0e1f68..ce4ee1180a 100644 --- a/src/components/material/MaterialTable.tsx +++ b/src/components/material/MaterialTable.tsx @@ -40,7 +40,7 @@ type State = { type IMaterialTableProps = IOwnProps; -interface IOwnProps { +export interface IOwnProps { handleCreateMaterialFolder?: (title: string) => void; handleDeleteMaterial?: (id: number) => void; handleDeleteMaterialFolder?: (id: number) => void; diff --git a/src/components/material/MaterialUpload.tsx b/src/components/material/MaterialUpload.tsx index 6a1122f5ed..f63b651ed4 100644 --- a/src/components/material/MaterialUpload.tsx +++ b/src/components/material/MaterialUpload.tsx @@ -5,7 +5,7 @@ import Dropzone from './Dropzone'; import { DirectoryData, MaterialData } from './materialShape'; import MaterialTable from './MaterialTable'; -interface IMaterialProps extends IDispatchProps, IStateProps {} +export interface IMaterialProps extends IDispatchProps, IStateProps {} export interface IDispatchProps { handleCreateMaterialFolder: (title: string) => void; diff --git a/src/components/material/__tests__/Material.tsx b/src/components/material/__tests__/Material.tsx new file mode 100644 index 0000000000..31ca519142 --- /dev/null +++ b/src/components/material/__tests__/Material.tsx @@ -0,0 +1,15 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; + +import Material, {IMaterialProps} from "../Material"; + +test('GameDev page renders correctly', () => { + const props: IMaterialProps = { + handleFetchMaterialIndex (p1: number) { + }, materialDirectoryTree: null, materialIndex: null + }; + const app = ; + const tree = shallow(app); + expect(tree.debug()).toMatchSnapshot(); +}); + diff --git a/src/components/material/__tests__/MaterialTable.tsx b/src/components/material/__tests__/MaterialTable.tsx new file mode 100644 index 0000000000..d076a4caa9 --- /dev/null +++ b/src/components/material/__tests__/MaterialTable.tsx @@ -0,0 +1,22 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; + +import MaterialTable, { IOwnProps } from "../MaterialTable"; + +const componentDidMountSpy = jest.fn(); + +jest.spyOn(MaterialTable.prototype, 'componentDidMount').mockImplementation(componentDidMountSpy); + +test('Material table renders correctly', () => { + const props: IOwnProps = { + handleCreateMaterialFolder (p1: string) { + }, handleDeleteMaterial (p1: number) { + }, handleDeleteMaterialFolder (p1: number) { + }, handleFetchMaterialIndex (p1: number) { + }, materialDirectoryTree: null, materialIndex: null + }; + const app = ; + const tree = shallow(app); + expect(tree.debug()).toMatchSnapshot(); + expect(componentDidMountSpy).toHaveBeenCalled(); +}); diff --git a/src/components/material/__tests__/MaterialUpload.tsx b/src/components/material/__tests__/MaterialUpload.tsx new file mode 100644 index 0000000000..4bf386422c --- /dev/null +++ b/src/components/material/__tests__/MaterialUpload.tsx @@ -0,0 +1,19 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; + +import MaterialUpload, {IMaterialProps} from "../MaterialUpload"; + +test("Story Upload area renders correctly", () => { + const props: IMaterialProps = { + handleCreateMaterialFolder (p1: string) { + }, handleDeleteMaterial (p1: number) { + }, handleDeleteMaterialFolder (p1: number) { + }, handleFetchMaterialIndex (p1: number) { + }, handleUploadMaterial (p1: File, p2: string, p3: string) { + }, materialDirectoryTree: null, materialIndex: null + + }; + const app = ; + const tree = shallow(app); + expect(tree.debug()).toMatchSnapshot(); +}); diff --git a/src/components/material/__tests__/__snapshots__/Material.tsx.snap b/src/components/material/__tests__/__snapshots__/Material.tsx.snap new file mode 100644 index 0000000000..d9284365fd --- /dev/null +++ b/src/components/material/__tests__/__snapshots__/Material.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GameDev page renders correctly 1`] = ` +"
+
+ +
+
" +`; diff --git a/src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap b/src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap new file mode 100644 index 0000000000..b65505b242 --- /dev/null +++ b/src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Material table renders correctly 1`] = ` +" +
+
+ + +
+ +
+
+ + Add New Folder + +
+ +
+ +
+
+
+ + Confirm + + + Cancel + +
+
+
+
+
+ +
+
+ +
+
+
+
" +`; diff --git a/src/components/material/__tests__/__snapshots__/MaterialUpload.tsx.snap b/src/components/material/__tests__/__snapshots__/MaterialUpload.tsx.snap new file mode 100644 index 0000000000..cd85d1b57d --- /dev/null +++ b/src/components/material/__tests__/__snapshots__/MaterialUpload.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Story Upload area renders correctly 1`] = ` +"
+
+ + + +
+
" +`; From 54826dca13709948d929ad6f527d3993a3db2b90 Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 2 Apr 2020 01:41:45 +0800 Subject: [PATCH 10/39] Deleted unnecessary page --- src/components/game-dev/GameDev.tsx | 33 ------------------- src/components/game-dev/__tests__/GameDev.tsx | 15 --------- .../__tests__/__snapshots__/GameDev.tsx.snap | 9 ----- src/containers/game-dev/StoryContainer.ts | 24 -------------- 4 files changed, 81 deletions(-) delete mode 100644 src/components/game-dev/GameDev.tsx delete mode 100644 src/components/game-dev/__tests__/GameDev.tsx delete mode 100644 src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap delete mode 100644 src/containers/game-dev/StoryContainer.ts diff --git a/src/components/game-dev/GameDev.tsx b/src/components/game-dev/GameDev.tsx deleted file mode 100644 index ef9d1f8a73..0000000000 --- a/src/components/game-dev/GameDev.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; - -import { DirectoryData, MaterialData } from './storyShape'; -import StoryTable from './StoryTable'; - -export interface IMaterialProps extends IDispatchProps, IStateProps {} - -export interface IDispatchProps { - handleFetchMaterialIndex: (id?: number) => void; -} - -export interface IStateProps { - materialDirectoryTree: DirectoryData[] | null; - materialIndex: MaterialData[] | null; -} - -class GameDev extends React.Component { - public render() { - return ( -
-
- -
-
- ); - } -} - -export default GameDev; diff --git a/src/components/game-dev/__tests__/GameDev.tsx b/src/components/game-dev/__tests__/GameDev.tsx deleted file mode 100644 index e10f27dbe0..0000000000 --- a/src/components/game-dev/__tests__/GameDev.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { shallow } from 'enzyme'; -import * as React from 'react'; - -import GameDev, { IMaterialProps } from "../GameDev"; - -test('GameDev page renders correctly', () => { - const props: IMaterialProps = { - handleFetchMaterialIndex (p1: number) { - }, materialDirectoryTree: null, materialIndex: null - }; - const app = ; - const tree = shallow(app); - expect(tree.debug()).toMatchSnapshot(); -}); - diff --git a/src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap b/src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap deleted file mode 100644 index 5925d11a5e..0000000000 --- a/src/components/game-dev/__tests__/__snapshots__/GameDev.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GameDev page renders correctly 1`] = ` -"
-
- -
-
" -`; diff --git a/src/containers/game-dev/StoryContainer.ts b/src/containers/game-dev/StoryContainer.ts deleted file mode 100644 index 79899e7e24..0000000000 --- a/src/containers/game-dev/StoryContainer.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; - -import { fetchMaterialIndex } from '../../actions'; -import GameDev, { IDispatchProps, IStateProps } from '../../components/game-dev/GameDev'; -import { IState } from '../../reducers/states'; - -const mapStateToProps: MapStateToProps = state => ({ - materialDirectoryTree: state.session.materialDirectoryTree, - materialIndex: state.session.materialIndex -}); - -const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => - bindActionCreators( - { - handleFetchMaterialIndex: (id?: number) => fetchMaterialIndex(id) - }, - dispatch - ); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(GameDev); From edb6be7278bf6ece84a58e3cf34a0822fa4ecda5 Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 2 Apr 2020 01:42:39 +0800 Subject: [PATCH 11/39] To use test bucket --- src/components/academy/game/backend/hosting.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/academy/game/backend/hosting.js b/src/components/academy/game/backend/hosting.js index e18a7a1286..5c875e051a 100644 --- a/src/components/academy/game/backend/hosting.js +++ b/src/components/academy/game/backend/hosting.js @@ -2,10 +2,11 @@ import { isStudent } from './user'; const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; +const LIVE_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/'; // placeholder URL // const TEST_ASSETS_HOST = 'https://localhost:8080/source-academy-assets/'; -const TEST_ASSETS_HOST = 'https://sa2021assets.blob.core.windows.net/sa2021-assets/'; +const TEST_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/testing-stories/'; // placeholder predicate export const ASSETS_HOST = LIVE_ASSETS_HOST; -export const STORY_HOST = isStudent() ? LIVE_ASSETS_HOST : TEST_ASSETS_HOST; \ No newline at end of file +export const STORY_HOST = isStudent() ? LIVE_STORIES_HOST : TEST_STORIES_HOST; \ No newline at end of file From faf82ae2701d478bdf0b0290c5fb6d7a8c1659b4 Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 2 Apr 2020 01:43:00 +0800 Subject: [PATCH 12/39] Changed test page to automatically find folder --- src/actions/actionTypes.ts | 3 + src/actions/material.ts | 2 + src/components/game-dev/StoryTable.tsx | 3 +- src/components/game-dev/StoryUpload.tsx | 4 +- .../game-dev/__tests__/StoryTable.tsx | 3 +- .../game-dev/__tests__/StoryUpload.tsx | 1 + .../game-dev/StoryUploadContainer.ts | 2 + src/sagas/backend.ts | 65 +++++++++++++++++++ 8 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index 98f087fd4c..f35f92875e 100755 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -121,3 +121,6 @@ export const FETCH_NOTIFICATIONS = 'FETCH_NOTIFICATIONS'; export const ACKNOWLEDGE_NOTIFICATIONS = 'ACKNOWLEDGE_NOTIFICATIONS'; export const UPDATE_NOTIFICATIONS = 'UPDATE_NOTIFICATIONS'; export const NOTIFY_CHATKIT_USERS = 'NOTIFY_CHATKIT_USERS'; + +/** GAMEDEV */ +export const FETCH_TEST_STORIES = 'FETCH_TEST_STORIES'; \ No newline at end of file diff --git a/src/actions/material.ts b/src/actions/material.ts index c766bf9585..87339488e7 100644 --- a/src/actions/material.ts +++ b/src/actions/material.ts @@ -21,3 +21,5 @@ export const updateMaterialIndex = (index: MaterialData[]) => export const uploadMaterial = (file: File, title: string, description: string) => action(actionTypes.UPLOAD_MATERIAL, { file, title, description }); + +export const fetchTestStories = () => action(actionTypes.FETCH_TEST_STORIES); \ No newline at end of file diff --git a/src/components/game-dev/StoryTable.tsx b/src/components/game-dev/StoryTable.tsx index d8588ba6f9..aadf4e3828 100644 --- a/src/components/game-dev/StoryTable.tsx +++ b/src/components/game-dev/StoryTable.tsx @@ -44,6 +44,7 @@ export interface IOwnProps { handleDeleteMaterial?: (id: number) => void; handleDeleteMaterialFolder?: (id: number) => void; handleFetchMaterialIndex: (id?: number) => void; + handleFetchTestStories: () => void; materialDirectoryTree: DirectoryData[] | null; materialIndex: MaterialData[] | null; } @@ -115,7 +116,7 @@ class StoryTable extends React.Component { } public componentDidMount() { - this.props.handleFetchMaterialIndex(); + this.props.handleFetchTestStories(); } public render() { diff --git a/src/components/game-dev/StoryUpload.tsx b/src/components/game-dev/StoryUpload.tsx index 0f613341a1..a876f0af1e 100644 --- a/src/components/game-dev/StoryUpload.tsx +++ b/src/components/game-dev/StoryUpload.tsx @@ -11,7 +11,8 @@ export interface IDispatchProps { handleCreateMaterialFolder: (title: string) => void; handleDeleteMaterial: (id: number) => void; handleDeleteMaterialFolder: (id: number) => void; - handleFetchMaterialIndex: (id?: number) => void; + handleFetchMaterialIndex: (id: number) => void; + handleFetchTestStories: () => void; handleUploadMaterial: (file: File, title: string, description: string) => void; } @@ -31,6 +32,7 @@ class StoryUpload extends React.Component { handleCreateMaterialFolder={this.props.handleCreateMaterialFolder} handleDeleteMaterial={this.props.handleDeleteMaterial} handleDeleteMaterialFolder={this.props.handleDeleteMaterialFolder} + handleFetchTestStories={this.props.handleFetchTestStories} handleFetchMaterialIndex={this.props.handleFetchMaterialIndex} materialDirectoryTree={this.props.materialDirectoryTree} materialIndex={this.props.materialIndex} diff --git a/src/components/game-dev/__tests__/StoryTable.tsx b/src/components/game-dev/__tests__/StoryTable.tsx index 4ae432bbc7..15361ed344 100644 --- a/src/components/game-dev/__tests__/StoryTable.tsx +++ b/src/components/game-dev/__tests__/StoryTable.tsx @@ -12,7 +12,8 @@ test('Story table renders correctly', () => { handleCreateMaterialFolder (p1: string) { }, handleDeleteMaterial (p1: number) { }, handleDeleteMaterialFolder (p1: number) { - }, handleFetchMaterialIndex (p1: number) { + }, handleFetchMaterialIndex (p1?: number) { + }, handleFetchTestStories () { }, materialDirectoryTree: null, materialIndex: null }; const app = ; diff --git a/src/components/game-dev/__tests__/StoryUpload.tsx b/src/components/game-dev/__tests__/StoryUpload.tsx index 925abbdbbe..4dacfb0d6d 100644 --- a/src/components/game-dev/__tests__/StoryUpload.tsx +++ b/src/components/game-dev/__tests__/StoryUpload.tsx @@ -9,6 +9,7 @@ test("Story Upload area renders correctly", () => { }, handleDeleteMaterial (p1: number) { }, handleDeleteMaterialFolder (p1: number) { }, handleFetchMaterialIndex (p1: number) { + }, handleFetchTestStories () { }, handleUploadMaterial (p1: File, p2: string, p3: string) { }, materialDirectoryTree: null, materialIndex: null diff --git a/src/containers/game-dev/StoryUploadContainer.ts b/src/containers/game-dev/StoryUploadContainer.ts index 6a32275691..1812f02dc3 100644 --- a/src/containers/game-dev/StoryUploadContainer.ts +++ b/src/containers/game-dev/StoryUploadContainer.ts @@ -6,6 +6,7 @@ import { deleteMaterial, deleteMaterialFolder, fetchMaterialIndex, + fetchTestStories, uploadMaterial } from '../../actions'; import StoryUpload, { @@ -25,6 +26,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Di handleCreateMaterialFolder: (title: string) => null, handleDeleteMaterial: (id: number) => deleteMaterial(id), handleDeleteMaterialFolder: (id: number) => deleteMaterialFolder(id), + handleFetchTestStories: () => fetchTestStories(), handleFetchMaterialIndex: (id?: number) => fetchMaterialIndex(id), handleUploadMaterial: (file: File, title: string, description: string) => uploadMaterial(file, title, description) diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index f7df616127..769a4e7824 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -17,6 +17,7 @@ import { IAssessmentOverview, IQuestion } from '../components/assessment/assessmentShape'; +import { MaterialData } from '../components/material/materialShape'; import { Notification, NotificationFilterFunction @@ -555,6 +556,70 @@ function* backendSaga(): SagaIterator { yield put(actions.fetchMaterialIndex(parentId)); yield call(showSuccessMessage, 'Deleted successfully!', 1000); }); + + yield takeEvery(actionTypes.FETCH_TEST_STORIES, function*( + action: ReturnType + ) { + + const fileName :string= "Test Stories"; + /* + yield put(actions.fetchMaterialIndex()); + let materialIndex = null; + while (materialIndex === null) { + materialIndex = yield select( + (state: IState) => state.session.materialIndex! + ); + } + let storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); + + if (storyFolder === undefined) { + yield put(actions.createMaterialFolder(fileName)); + while (yield materialIndex.find((x :MaterialData) => x.title === fileName) === undefined) { + materialIndex = yield select( + (state: IState) => state.session.materialIndex! + ); + } + storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); + } + // tslint:disable-next-line:no-console + console.log(storyFolder.id == null ? 1 : 0); + yield put(actions.fetchMaterialIndex(storyFolder.id)); + */ + + const tokens = yield select((state: IState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + let resp = yield call(request.getMaterialIndex, -1, tokens); + if (resp) { + let materialIndex = resp.index; + let storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); + if (storyFolder === undefined) { + const role = yield select((state: IState) => state.session.role!); + if (role === Role.Student) { + return yield call(showWarningMessage, 'Only staff can create materials folder.'); + } + resp = yield request.postMaterialFolder(fileName, -1, tokens); + if (!resp || !resp.ok) { + yield request.handleResponseError(resp); + return; + } + } + + resp = yield call(request.getMaterialIndex, -1, tokens); + if (resp) { + materialIndex = resp.index; + storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); + resp = yield call(request.getMaterialIndex, storyFolder.id, tokens); + if(resp) { + const directory_tree = resp.directory_tree; + materialIndex = resp.index; + yield put(actions.updateMaterialDirectoryTree(directory_tree)); + yield put(actions.updateMaterialIndex(materialIndex)); + } + } + } + }); } export default backendSaga; From bdd2fb946cf902d2b5fd03a2d2f6c323c9279dd6 Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 2 Apr 2020 01:47:34 +0800 Subject: [PATCH 13/39] Prettified and Updated Snapshots --- src/actions/actionTypes.ts | 2 +- src/actions/material.ts | 2 +- src/components/academy/NavigationBar.tsx | 2 -- src/components/academy/index.tsx | 2 +- src/components/game-dev/StoryTable.tsx | 6 +++--- .../game-dev/__tests__/StoryTable.tsx | 15 +++++++------- .../game-dev/__tests__/StoryUpload.tsx | 20 +++++++++---------- .../__snapshots__/StoryUpload.tsx.snap | 2 +- .../material/__tests__/Material.tsx | 8 ++++---- .../material/__tests__/MaterialTable.tsx | 13 ++++++------ .../material/__tests__/MaterialUpload.tsx | 18 ++++++++--------- .../game-dev/StoryUploadContainer.ts | 5 +---- src/sagas/backend.ts | 11 +++++----- 13 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index f35f92875e..2ef04053b8 100755 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -123,4 +123,4 @@ export const UPDATE_NOTIFICATIONS = 'UPDATE_NOTIFICATIONS'; export const NOTIFY_CHATKIT_USERS = 'NOTIFY_CHATKIT_USERS'; /** GAMEDEV */ -export const FETCH_TEST_STORIES = 'FETCH_TEST_STORIES'; \ No newline at end of file +export const FETCH_TEST_STORIES = 'FETCH_TEST_STORIES'; diff --git a/src/actions/material.ts b/src/actions/material.ts index 87339488e7..487b029e72 100644 --- a/src/actions/material.ts +++ b/src/actions/material.ts @@ -22,4 +22,4 @@ export const updateMaterialIndex = (index: MaterialData[]) => export const uploadMaterial = (file: File, title: string, description: string) => action(actionTypes.UPLOAD_MATERIAL, { file, title, description }); -export const fetchTestStories = () => action(actionTypes.FETCH_TEST_STORIES); \ No newline at end of file +export const fetchTestStories = () => action(actionTypes.FETCH_TEST_STORIES); diff --git a/src/components/academy/NavigationBar.tsx b/src/components/academy/NavigationBar.tsx index 2db2dc7185..ce53170375 100644 --- a/src/components/academy/NavigationBar.tsx +++ b/src/components/academy/NavigationBar.tsx @@ -120,8 +120,6 @@ const NavigationBar: React.SFC = props => (
Game Dev
- - ) : null} diff --git a/src/components/academy/index.tsx b/src/components/academy/index.tsx index 6b37836af1..4a69c65955 100644 --- a/src/components/academy/index.tsx +++ b/src/components/academy/index.tsx @@ -3,7 +3,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router'; import Grading from '../../containers/academy/grading'; import AssessmentContainer from '../../containers/assessment'; -import StoryUpload from "../../containers/game-dev/StoryUploadContainer"; +import StoryUpload from '../../containers/game-dev/StoryUploadContainer'; import Game from '../../containers/GameContainer'; import MaterialUpload from '../../containers/material/MaterialUploadContainer'; import Sourcereel from '../../containers/sourcecast/SourcereelContainer'; diff --git a/src/components/game-dev/StoryTable.tsx b/src/components/game-dev/StoryTable.tsx index aadf4e3828..c7d36bd163 100644 --- a/src/components/game-dev/StoryTable.tsx +++ b/src/components/game-dev/StoryTable.tsx @@ -175,7 +175,7 @@ class StoryTable extends React.Component { - {" "} + {' '} @@ -208,11 +208,11 @@ class StoryTable extends React.Component { }; private handleTest = () => { - window.open("/academy/game"); + window.open('/academy/game'); }; private handleReset = () => { - window.alert("Are you sure you want to discard all changes made?"); + window.alert('Are you sure you want to discard all changes made?'); }; private onGridReady = (params: GridReadyEvent) => { diff --git a/src/components/game-dev/__tests__/StoryTable.tsx b/src/components/game-dev/__tests__/StoryTable.tsx index 15361ed344..9ce430e508 100644 --- a/src/components/game-dev/__tests__/StoryTable.tsx +++ b/src/components/game-dev/__tests__/StoryTable.tsx @@ -1,7 +1,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import StoryTable, { IOwnProps } from "../StoryTable"; +import StoryTable, { IOwnProps } from '../StoryTable'; const componentDidMountSpy = jest.fn(); @@ -9,12 +9,13 @@ jest.spyOn(StoryTable.prototype, 'componentDidMount').mockImplementation(compone test('Story table renders correctly', () => { const props: IOwnProps = { - handleCreateMaterialFolder (p1: string) { - }, handleDeleteMaterial (p1: number) { - }, handleDeleteMaterialFolder (p1: number) { - }, handleFetchMaterialIndex (p1?: number) { - }, handleFetchTestStories () { - }, materialDirectoryTree: null, materialIndex: null + handleCreateMaterialFolder(p1: string) {}, + handleDeleteMaterial(p1: number) {}, + handleDeleteMaterialFolder(p1: number) {}, + handleFetchMaterialIndex(p1?: number) {}, + handleFetchTestStories() {}, + materialDirectoryTree: null, + materialIndex: null }; const app = ; const tree = shallow(app); diff --git a/src/components/game-dev/__tests__/StoryUpload.tsx b/src/components/game-dev/__tests__/StoryUpload.tsx index 4dacfb0d6d..6a0eb90f45 100644 --- a/src/components/game-dev/__tests__/StoryUpload.tsx +++ b/src/components/game-dev/__tests__/StoryUpload.tsx @@ -1,18 +1,18 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import StoryUpload, { IStoryProps } from "../StoryUpload"; +import StoryUpload, { IStoryProps } from '../StoryUpload'; -test("Story Upload area renders correctly", () => { +test('Story Upload area renders correctly', () => { const props: IStoryProps = { - handleCreateMaterialFolder (p1: string) { - }, handleDeleteMaterial (p1: number) { - }, handleDeleteMaterialFolder (p1: number) { - }, handleFetchMaterialIndex (p1: number) { - }, handleFetchTestStories () { - }, handleUploadMaterial (p1: File, p2: string, p3: string) { - }, materialDirectoryTree: null, materialIndex: null - + handleCreateMaterialFolder(p1: string) {}, + handleDeleteMaterial(p1: number) {}, + handleDeleteMaterialFolder(p1: number) {}, + handleFetchMaterialIndex(p1: number) {}, + handleFetchTestStories() {}, + handleUploadMaterial(p1: File, p2: string, p3: string) {}, + materialDirectoryTree: null, + materialIndex: null }; const app = ; const tree = shallow(app); diff --git a/src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap b/src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap index 775f3703b7..03345312b5 100644 --- a/src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap +++ b/src/components/game-dev/__tests__/__snapshots__/StoryUpload.tsx.snap @@ -5,7 +5,7 @@ exports[`Story Upload area renders correctly 1`] = `
- +
" `; diff --git a/src/components/material/__tests__/Material.tsx b/src/components/material/__tests__/Material.tsx index 31ca519142..e4919e94a4 100644 --- a/src/components/material/__tests__/Material.tsx +++ b/src/components/material/__tests__/Material.tsx @@ -1,15 +1,15 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import Material, {IMaterialProps} from "../Material"; +import Material, { IMaterialProps } from '../Material'; test('GameDev page renders correctly', () => { const props: IMaterialProps = { - handleFetchMaterialIndex (p1: number) { - }, materialDirectoryTree: null, materialIndex: null + handleFetchMaterialIndex(p1: number) {}, + materialDirectoryTree: null, + materialIndex: null }; const app = ; const tree = shallow(app); expect(tree.debug()).toMatchSnapshot(); }); - diff --git a/src/components/material/__tests__/MaterialTable.tsx b/src/components/material/__tests__/MaterialTable.tsx index d076a4caa9..c7523d2921 100644 --- a/src/components/material/__tests__/MaterialTable.tsx +++ b/src/components/material/__tests__/MaterialTable.tsx @@ -1,7 +1,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import MaterialTable, { IOwnProps } from "../MaterialTable"; +import MaterialTable, { IOwnProps } from '../MaterialTable'; const componentDidMountSpy = jest.fn(); @@ -9,11 +9,12 @@ jest.spyOn(MaterialTable.prototype, 'componentDidMount').mockImplementation(comp test('Material table renders correctly', () => { const props: IOwnProps = { - handleCreateMaterialFolder (p1: string) { - }, handleDeleteMaterial (p1: number) { - }, handleDeleteMaterialFolder (p1: number) { - }, handleFetchMaterialIndex (p1: number) { - }, materialDirectoryTree: null, materialIndex: null + handleCreateMaterialFolder(p1: string) {}, + handleDeleteMaterial(p1: number) {}, + handleDeleteMaterialFolder(p1: number) {}, + handleFetchMaterialIndex(p1: number) {}, + materialDirectoryTree: null, + materialIndex: null }; const app = ; const tree = shallow(app); diff --git a/src/components/material/__tests__/MaterialUpload.tsx b/src/components/material/__tests__/MaterialUpload.tsx index 4bf386422c..ef9a26bf16 100644 --- a/src/components/material/__tests__/MaterialUpload.tsx +++ b/src/components/material/__tests__/MaterialUpload.tsx @@ -1,17 +1,17 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import MaterialUpload, {IMaterialProps} from "../MaterialUpload"; +import MaterialUpload, { IMaterialProps } from '../MaterialUpload'; -test("Story Upload area renders correctly", () => { +test('Story Upload area renders correctly', () => { const props: IMaterialProps = { - handleCreateMaterialFolder (p1: string) { - }, handleDeleteMaterial (p1: number) { - }, handleDeleteMaterialFolder (p1: number) { - }, handleFetchMaterialIndex (p1: number) { - }, handleUploadMaterial (p1: File, p2: string, p3: string) { - }, materialDirectoryTree: null, materialIndex: null - + handleCreateMaterialFolder(p1: string) {}, + handleDeleteMaterial(p1: number) {}, + handleDeleteMaterialFolder(p1: number) {}, + handleFetchMaterialIndex(p1: number) {}, + handleUploadMaterial(p1: File, p2: string, p3: string) {}, + materialDirectoryTree: null, + materialIndex: null }; const app = ; const tree = shallow(app); diff --git a/src/containers/game-dev/StoryUploadContainer.ts b/src/containers/game-dev/StoryUploadContainer.ts index 1812f02dc3..afd9a9c1b2 100644 --- a/src/containers/game-dev/StoryUploadContainer.ts +++ b/src/containers/game-dev/StoryUploadContainer.ts @@ -9,10 +9,7 @@ import { fetchTestStories, uploadMaterial } from '../../actions'; -import StoryUpload, { - IDispatchProps, - IStateProps -} from '../../components/game-dev/StoryUpload'; +import StoryUpload, { IDispatchProps, IStateProps } from '../../components/game-dev/StoryUpload'; import { IState } from '../../reducers/states'; const mapStateToProps: MapStateToProps = state => ({ diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index 769a4e7824..43f4447981 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -556,12 +556,11 @@ function* backendSaga(): SagaIterator { yield put(actions.fetchMaterialIndex(parentId)); yield call(showSuccessMessage, 'Deleted successfully!', 1000); }); - + yield takeEvery(actionTypes.FETCH_TEST_STORIES, function*( action: ReturnType ) { - - const fileName :string= "Test Stories"; + const fileName: string = 'Test Stories'; /* yield put(actions.fetchMaterialIndex()); let materialIndex = null; @@ -593,7 +592,7 @@ function* backendSaga(): SagaIterator { let resp = yield call(request.getMaterialIndex, -1, tokens); if (resp) { let materialIndex = resp.index; - let storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); + let storyFolder = yield materialIndex.find((x: MaterialData) => x.title === fileName); if (storyFolder === undefined) { const role = yield select((state: IState) => state.session.role!); if (role === Role.Student) { @@ -609,9 +608,9 @@ function* backendSaga(): SagaIterator { resp = yield call(request.getMaterialIndex, -1, tokens); if (resp) { materialIndex = resp.index; - storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); + storyFolder = yield materialIndex.find((x: MaterialData) => x.title === fileName); resp = yield call(request.getMaterialIndex, storyFolder.id, tokens); - if(resp) { + if (resp) { const directory_tree = resp.directory_tree; materialIndex = resp.index; yield put(actions.updateMaterialDirectoryTree(directory_tree)); From 0a91e9fa54ef7af96e5673b4a3f8738320f86fc9 Mon Sep 17 00:00:00 2001 From: wltan Date: Thu, 2 Apr 2020 16:01:18 +0800 Subject: [PATCH 14/39] Change the story retrieval workflow Dev users will first attempt to pull from the test bucket. If it fails, then try the live bucket. Student users will always pull from the live bucket. --- .../academy/game/backend/game-state.js | 29 ++++++++++++------- .../academy/game/backend/hosting.js | 10 ++----- .../academy/game/constants/constants.js | 5 ++-- .../game/story-manager/story-manager.js | 21 ++++++++++---- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 30380594f6..7bf82039be 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -1,4 +1,5 @@ -import {storyXMLPath} from '../constants/constants' +import { storyXMLPathTest, storyXMLPathLive } from '../constants/constants' +import { isStudent } from './user'; /** * Handles data regarding the game state. @@ -67,23 +68,29 @@ function getStudentMissionPointer() { let stories = []; function fetchGlobalMissionPointer(callback) { - $.ajax({ + const makeAjax = isTest => $.ajax({ type: 'GET', - url: storyXMLPath + 'master.xml', + url: (isTest ? storyXMLPathTest : storyXMLPathLive) + 'master.xml', dataType: 'xml', success: xml => { stories = Array.from(xml.children[0].children); stories = stories.sort((a, b) => parseInt(a.getAttribute("key")) - parseInt(b.getAttribute("key"))); + const now = currentDateOverride ? currentDateOverride : new Date(); + const openStory = story => new Date(story.getAttribute("startDate")) < now && now < new Date(story.getAttribute("endDate")); + stories = stories.filter(openStory); + callback(); }, - error: () => { - console.error('Cannot find master story list'); - } - }).then(() => { - const now = currentDateOverride ? currentDateOverride : new Date(); - const openStory = story => new Date(story.getAttribute("startDate")) < now && now < new Date(story.getAttribute("endDate")); - stories = stories.filter(openStory); - callback(); + error: isTest + ? () => { + console.log('Cannot find master story list on test'); + console.log('Trying on live...'); + makeAjax(false); + } + : () => { + console.error('Cannot find master story list'); + } }); + makeAjax(!isStudent()); } /** diff --git a/src/components/academy/game/backend/hosting.js b/src/components/academy/game/backend/hosting.js index 5c875e051a..fa9b2b66f0 100644 --- a/src/components/academy/game/backend/hosting.js +++ b/src/components/academy/game/backend/hosting.js @@ -2,11 +2,7 @@ import { isStudent } from './user'; const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; -const LIVE_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/'; -// placeholder URL -// const TEST_ASSETS_HOST = 'https://localhost:8080/source-academy-assets/'; -const TEST_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/testing-stories/'; +export const LIVE_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/'; +export const TEST_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/testing-stories/'; -// placeholder predicate -export const ASSETS_HOST = LIVE_ASSETS_HOST; -export const STORY_HOST = isStudent() ? LIVE_STORIES_HOST : TEST_STORIES_HOST; \ No newline at end of file +export const ASSETS_HOST = LIVE_ASSETS_HOST; \ No newline at end of file diff --git a/src/components/academy/game/constants/constants.js b/src/components/academy/game/constants/constants.js index 0afaca2514..561b4cbd25 100644 --- a/src/components/academy/game/constants/constants.js +++ b/src/components/academy/game/constants/constants.js @@ -1,4 +1,4 @@ -import {ASSETS_HOST, STORY_HOST} from '../backend/hosting' +import { ASSETS_HOST, LIVE_STORIES_HOST, TEST_STORIES_HOST } from '../backend/hosting' export const screenWidth = 1920, @@ -16,7 +16,8 @@ export const playerAvatarOffset = 40, glowDistance = 30, textSpeed = 0.02, - storyXMLPath = STORY_HOST + 'stories/', + storyXMLPathLive = LIVE_STORIES_HOST + 'stories/', + storyXMLPathTest = TEST_STORIES_HOST + 'stories/', locationPath = ASSETS_HOST + 'locations/', objectPath = ASSETS_HOST + 'objects/', imgPath = ASSETS_HOST + 'images/', diff --git a/src/components/academy/game/story-manager/story-manager.js b/src/components/academy/game/story-manager/story-manager.js index dc65368894..1d09839a79 100644 --- a/src/components/academy/game/story-manager/story-manager.js +++ b/src/components/academy/game/story-manager/story-manager.js @@ -1,4 +1,5 @@ import * as PIXI from 'pixi.js' +import { isStudent } from '../backend/user.js'; var Constants = require('../constants/constants.js'); var QuestManager = require('../quest-manager/quest-manager.js'); @@ -121,9 +122,10 @@ export function loadStoryXML(storyXMLs, willSave, callback) { } else { // download the story downloadRequestSent[curId] = true; - $.ajax({ + const makeAjax = isTest => $.ajax({ type: 'GET', - url: Constants.storyXMLPath + curId + '.story.xml', + url: (isTest ? Constants.storyXMLPathTest : Constants.storyXMLPathLive) + + curId + '.story.xml', dataType: 'xml', success: function(xml) { var story = xml.children[0]; @@ -155,11 +157,18 @@ export function loadStoryXML(storyXMLs, willSave, callback) { } } }, - error: function() { - loadingOverlay.visible = false; - console.error('Cannot find story ' + curId); - } + error: isTest + ? () => { + console.log('Cannot find story ' + curId + ' on test'); + console.log('Trying on live...'); + makeAjax(false); + } + : () => { + loadingOverlay.visible = false; + console.error('Cannot find story ' + curId); + } }); + makeAjax(!isStudent()); download(i + 1, storyXMLs, callback); } } From 841f602d3193b1fb7b9bb727470914fc05604d79 Mon Sep 17 00:00:00 2001 From: travisryte Date: Mon, 6 Apr 2020 17:57:26 +0800 Subject: [PATCH 15/39] Fixed location of stories --- src/components/academy/game/backend/hosting.js | 4 ++-- src/components/academy/game/constants/constants.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/academy/game/backend/hosting.js b/src/components/academy/game/backend/hosting.js index fa9b2b66f0..8b42f6ff29 100644 --- a/src/components/academy/game/backend/hosting.js +++ b/src/components/academy/game/backend/hosting.js @@ -2,7 +2,7 @@ import { isStudent } from './user'; const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; -export const LIVE_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/'; -export const TEST_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/testing-stories/'; +export const TEST_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/'; +export const LIVE_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/live-stories/'; export const ASSETS_HOST = LIVE_ASSETS_HOST; \ No newline at end of file diff --git a/src/components/academy/game/constants/constants.js b/src/components/academy/game/constants/constants.js index 561b4cbd25..db1135a92b 100644 --- a/src/components/academy/game/constants/constants.js +++ b/src/components/academy/game/constants/constants.js @@ -16,8 +16,8 @@ export const playerAvatarOffset = 40, glowDistance = 30, textSpeed = 0.02, - storyXMLPathLive = LIVE_STORIES_HOST + 'stories/', - storyXMLPathTest = TEST_STORIES_HOST + 'stories/', + storyXMLPathLive = LIVE_STORIES_HOST, + storyXMLPathTest = TEST_STORIES_HOST, locationPath = ASSETS_HOST + 'locations/', objectPath = ASSETS_HOST + 'objects/', imgPath = ASSETS_HOST + 'images/', From fbe3460442ee5e0d6c6d62ccf72e76969e05853b Mon Sep 17 00:00:00 2001 From: Asthenosphere Date: Fri, 10 Apr 2020 14:11:25 +0800 Subject: [PATCH 16/39] Json Upload --- src/components/game-dev/JsonUpload.tsx | 32 ++++++++++++++++++++++++++ src/components/game-dev/StoryTable.tsx | 3 +++ 2 files changed, 35 insertions(+) create mode 100644 src/components/game-dev/JsonUpload.tsx diff --git a/src/components/game-dev/JsonUpload.tsx b/src/components/game-dev/JsonUpload.tsx new file mode 100644 index 0000000000..49e66a8239 --- /dev/null +++ b/src/components/game-dev/JsonUpload.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +class JsonUpload extends React.Component { + private static onFormSubmit(e: { preventDefault: () => void; }){ + e.preventDefault(); // Stop form submit + } + + constructor(props: Readonly<{}>) { + super(props); + this.state ={ + file:null + }; + JsonUpload.onFormSubmit = JsonUpload.onFormSubmit.bind(this); + this.onChange = this.onChange.bind(this); + } + + public render() { + return ( +
+

Json File Upload

+ + +
+ ); + } + private onChange(e: { target: { files: any; }; }) { + this.setState({file:e.target.files[0]}); + } +} + + +export default JsonUpload; diff --git a/src/components/game-dev/StoryTable.tsx b/src/components/game-dev/StoryTable.tsx index c7d36bd163..8c8c6aa546 100644 --- a/src/components/game-dev/StoryTable.tsx +++ b/src/components/game-dev/StoryTable.tsx @@ -23,6 +23,7 @@ import { getStandardDateTime } from '../../utils/dateHelpers'; import { controlButton } from '../commons'; import DeleteCell from './DeleteCell'; import DownloadCell from './DownloadCell'; +import JsonUpload from "./JsonUpload"; import { DirectoryData, MaterialData } from './storyShape'; /** @@ -172,6 +173,8 @@ class StoryTable extends React.Component { /> + +
From e06bcba44b323f18e92b206a032102242cf11ea7 Mon Sep 17 00:00:00 2001 From: Asthenosphere Date: Fri, 10 Apr 2020 14:23:47 +0800 Subject: [PATCH 17/39] Modify test for StoryTable --- .../game-dev/__tests__/__snapshots__/StoryTable.tsx.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap b/src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap index 312fc94533..a703277c0e 100644 --- a/src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap +++ b/src/components/game-dev/__tests__/__snapshots__/StoryTable.tsx.snap @@ -20,6 +20,8 @@ exports[`Story table renders correctly 1`] = ` + +
Open Game in new Window From f9f7d1600a1ee3146e9019c4ba50a2781f67946c Mon Sep 17 00:00:00 2001 From: wltan <53135010+wltan@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:13:47 +0800 Subject: [PATCH 18/39] Read input JSON file and set override --- src/components/game-dev/JsonUpload.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/game-dev/JsonUpload.tsx b/src/components/game-dev/JsonUpload.tsx index 49e66a8239..3f013d0c86 100644 --- a/src/components/game-dev/JsonUpload.tsx +++ b/src/components/game-dev/JsonUpload.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { overrideStudentData } from '../academy/game/backend/game-state.js'; class JsonUpload extends React.Component { private static onFormSubmit(e: { preventDefault: () => void; }){ @@ -7,9 +8,7 @@ class JsonUpload extends React.Component { constructor(props: Readonly<{}>) { super(props); - this.state ={ - file:null - }; + overrideStudentData(undefined); JsonUpload.onFormSubmit = JsonUpload.onFormSubmit.bind(this); this.onChange = this.onChange.bind(this); } @@ -17,14 +16,17 @@ class JsonUpload extends React.Component { public render() { return (
-

Json File Upload

+

Game State Override

-
); } private onChange(e: { target: { files: any; }; }) { - this.setState({file:e.target.files[0]}); + const reader = new FileReader(); + reader.onload = (event: Event) => { + overrideStudentData(JSON.parse(""+reader.result)); + }; + reader.readAsText(e.target.files[0]); } } From e53e689f057899f6a3cfe56938c35adafe1cf015 Mon Sep 17 00:00:00 2001 From: wltan <53135010+wltan@users.noreply.github.com> Date: Fri, 10 Apr 2020 17:25:19 +0800 Subject: [PATCH 19/39] Enable the JSON file to simulate customized mission pointer and current date as well --- src/components/academy/game/backend/game-state.js | 14 +++++++++----- src/components/game-dev/JsonUpload.tsx | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 7bf82039be..4150c33df2 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -28,11 +28,15 @@ let studentDataOverride = undefined, missionPointerOverride = undefined, currentDateOverride = undefined; // override student game data -export function overrideStudentData(data) { studentDataOverride = data; } -// override student's current mission -export function overrideMissionPointer(data) { missionPointerOverride = data; } -// override current date (to determine active missions) -export function overrideCurrentDate(data) { currentDateOverride = data; } +export function overrideGameState(data) { + if (data) { + studentDataOverride = data; + missionPointerOverride = data.missionPointer; + currentDateOverride = data.currentDate; + } else { + studentDataOverride = missionPointerOverride = currentDateOverride = undefined; + } +} export function getStudentData() { // formerly create-initializer/loadFromServer diff --git a/src/components/game-dev/JsonUpload.tsx b/src/components/game-dev/JsonUpload.tsx index 3f013d0c86..7bd27b1c6a 100644 --- a/src/components/game-dev/JsonUpload.tsx +++ b/src/components/game-dev/JsonUpload.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { overrideStudentData } from '../academy/game/backend/game-state.js'; +import { overrideGameState } from '../academy/game/backend/game-state.js'; class JsonUpload extends React.Component { private static onFormSubmit(e: { preventDefault: () => void; }){ @@ -8,7 +8,7 @@ class JsonUpload extends React.Component { constructor(props: Readonly<{}>) { super(props); - overrideStudentData(undefined); + overrideGameState(undefined); JsonUpload.onFormSubmit = JsonUpload.onFormSubmit.bind(this); this.onChange = this.onChange.bind(this); } @@ -24,7 +24,7 @@ class JsonUpload extends React.Component { private onChange(e: { target: { files: any; }; }) { const reader = new FileReader(); reader.onload = (event: Event) => { - overrideStudentData(JSON.parse(""+reader.result)); + overrideGameState(JSON.parse(""+reader.result)); }; reader.readAsText(e.target.files[0]); } From 586999c4396c48d3e4c1054414296e943856a4bf Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 16 Apr 2020 04:09:59 +0800 Subject: [PATCH 20/39] Fixed bug where tests on PixiJS will fail due to canvas error Tried https://github.com/pixijs/pixi.js/issues/4769 after encountering TypeError: Cannot set property 'fillStyle' of null It solved the problem with the game-dev component. --- package-lock.json | 34 ++++++++++++++++++++++++++++++++++ package.json | 1 + src/setupTests.ts | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 103c0c176a..893ac6e23b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4766,6 +4766,12 @@ "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", "dev": true }, + "cssfontparser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz", + "integrity": "sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=", + "dev": true + }, "cssnano": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", @@ -9504,6 +9510,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-22.4.2.tgz", "integrity": "sha512-wD7dXWtfaQAgbNVsjFqzmuhg6nzwGsTRVea3FpSJ7GURhG+J536fw4mdoLB01DgiEozDDeF1ZMR/UlUszTsCrg==", "dev": true, + "setupFiles": ["jest-canvas-mock"], "requires": { "import-local": "^1.0.0", "jest-cli": "^22.4.2" @@ -9774,6 +9781,16 @@ } } }, + "jest-canvas-mock": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz", + "integrity": "sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw==", + "dev": true, + "requires": { + "cssfontparser": "^1.2.1", + "parse-color": "^1.0.0" + } + }, "jest-changed-files": { "version": "22.4.3", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-22.4.3.tgz", @@ -15923,6 +15940,23 @@ "pbkdf2": "^3.0.3" } }, + "parse-color": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", + "integrity": "sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=", + "dev": true, + "requires": { + "color-convert": "~0.5.0" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=", + "dev": true + } + } + }, "parse-github-url": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", diff --git a/package.json b/package.json index 453837bbed..cd067ec87e 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.14.0", "husky": "^1.3.1", + "jest-canvas-mock": "^2.2.0", "local-cors-proxy": "^1.0.2", "prettier": "^1.18.2", "react-scripts-ts": "^2.16.0", diff --git a/src/setupTests.ts b/src/setupTests.ts index b8acf7f719..2b4b22ba41 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,4 +1,4 @@ import { configure } from 'enzyme'; import * as Adapter from 'enzyme-adapter-react-16'; - +import 'jest-canvas-mock'; configure({ adapter: new Adapter() }); From fd88f3f729041bdf1fd39987382bf0d175b337a0 Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 16 Apr 2020 04:24:03 +0800 Subject: [PATCH 21/39] Fully moved completed-quests and collectibles away from localStorage. Saving and loading of quest and collectibles will come through the user data or from overriding data. --- .../academy/game/backend/game-state.js | 77 +++++++++++-------- .../game/object-manager/object-manager.js | 5 +- .../game/quest-manager/quest-manager.js | 3 +- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 4150c33df2..50e8540dee 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -1,72 +1,88 @@ import { storyXMLPathTest, storyXMLPathLive } from '../constants/constants' import { isStudent } from './user'; +var SaveManager = require('../save-manager/save-manager.js'); + /** * Handles data regarding the game state. * - The student's list of completed quests and collectibles * - The student's current story mission * - The global list of missions that are open + * - The action to save user state to server. */ let fetched = false; -let studentMissionPointer = undefined, - studentData = undefined; -export function fetchGameData(userStory, callback) { +let studentData = undefined, + handleSaveData = undefined, + studentStory = undefined; + +export function fetchGameData(userStory, gameState, callback) { // fetch only needs to be called once; if there are additional calls somehow then ignore them if(fetched) { callback(); return; } fetched = true; - studentMissionPointer = userStory.story; - // not implemented yet - studentData = undefined; // userStory.data; + studentStory = userStory.story; + studentData = gameState; fetchGlobalMissionPointer(callback); } // overrides let studentDataOverride = undefined, - missionPointerOverride = undefined, - currentDateOverride = undefined; + currentDateOverride = undefined, + studentStoryOverride = undefined; + // override student game data export function overrideGameState(data) { if (data) { - studentDataOverride = data; - missionPointerOverride = data.missionPointer; + studentDataOverride = data.gameState; + studentStoryOverride = data.story; currentDateOverride = data.currentDate; } else { - studentDataOverride = missionPointerOverride = currentDateOverride = undefined; + studentStoryOverride = studentDataOverride = missionPointerOverride = currentDateOverride = undefined; } } +export function setSaveHandler(saveData) { + handleSaveData = saveData; +} + export function getStudentData() { // formerly create-initializer/loadFromServer if(studentDataOverride) return studentDataOverride; return studentData; } -export function saveStudentData(json) { - // formerly create-initializer/saveToServer - return json; +export function saveStudentData(data) { + console.log('saving student data'); + if (handleSaveData !== undefined) { + handleSaveData(data) + } } export function saveCollectible(collectible) { - // currently local but we should eventually migrate to backend - if (typeof Storage !== 'undefined') { - localStorage.setItem(collectible, 'collected'); - } + studentData.collectibles[collectible] = 'completed'; + saveStudentData(studentData); +} + +export function hasCollectible(collectible) { + return studentData && + studentData.collectibles[collectible] && + studentData.collectibles[collectible] === 'completed'; } export function saveQuest(questId) { - // currently local but we should eventually migrate to backend - if (typeof Storage !== 'undefined') { - localStorage.setItem(questId, 'completed'); - } + studentData.completed_quests.push(questId); + saveStudentData(studentData); +} + +export function hasCompletedQuest(questId) { + return studentData && studentData.completed_quests.includes(questId); } -function getStudentMissionPointer() { - // placeholder - if(missionPointerOverride) return missionPointerOverride; - return studentMissionPointer; +function getStudentStory() { + if(studentStoryOverride) return studentStoryOverride; + return studentStory; } let stories = []; @@ -103,12 +119,13 @@ function fetchGlobalMissionPointer(callback) { * global list of open missions, then the corresponding upper (or lower) bound will be used. */ export function getMissionPointer() { - let student = getStudentMissionPointer(); + //finds the mission id's mission pointer + let missionPointer = stories.find(story => story.getAttribute("id") === getStudentStory()); const newest = parseInt(stories[stories.length-1].getAttribute("key")); // the newest mission to open const oldest = parseInt(stories[0].getAttribute("key")); // the oldest mission to open - student = Math.min(student, newest); - student = Math.max(student, oldest); - const storyToLoad = stories.filter(story => story.getAttribute("key") == student)[0]; + missionPointer = Math.min(missionPointer, newest); + missionPointer = Math.max(missionPointer, oldest); + const storyToLoad = stories.filter(story => story.getAttribute("key") == missionPointer)[0]; console.log("Now loading story " + storyToLoad.getAttribute("id")); // debug statement return storyToLoad.getAttribute("id"); } \ No newline at end of file diff --git a/src/components/academy/game/object-manager/object-manager.js b/src/components/academy/game/object-manager/object-manager.js index f464ad7a49..6fdcf6f580 100644 --- a/src/components/academy/game/object-manager/object-manager.js +++ b/src/components/academy/game/object-manager/object-manager.js @@ -10,6 +10,7 @@ var ExternalManager = require('../external-manager/external-manager.js'); var MapOverlay = require('../map-overlay/map-overlay.js'); var Utils = require('../utils/utils.js'); var FilterEffects = require('../filter-effects/filter-effects.js'); +var GameState = require('../backend/game-state') var mapObjects; var sequenceObjects; @@ -164,8 +165,8 @@ export function processTempObject(gameLocation, node) { } var collectible = node.getAttribute('name'); var isInDorm = gameLocation.name == 'yourRoom'; - if ((isInDorm && !localStorage.hasOwnProperty(collectible))|| - (!isInDorm && localStorage.hasOwnProperty(collectible))) { + if ((isInDorm && !GameState.hasColletible(collectible))|| + (!isInDorm && GameState.hasColletible(collectible))) { return; //don't load the collectible in dorm if it's not collected || don't load if in hidden location and collected } if (isInDorm) { diff --git a/src/components/academy/game/quest-manager/quest-manager.js b/src/components/academy/game/quest-manager/quest-manager.js index b2e8c4d301..00ad1b56af 100644 --- a/src/components/academy/game/quest-manager/quest-manager.js +++ b/src/components/academy/game/quest-manager/quest-manager.js @@ -6,6 +6,7 @@ var StoryManager = require('../story-manager/story-manager.js'); var SaveManager = require('../save-manager/save-manager.js'); var Utils = require('../utils/utils.js'); var ExternalManager = require('../external-manager/external-manager.js'); +var GameState = require('../backend/game-state'); var loadedQuests = {}; var activeQuests = {}; @@ -65,7 +66,7 @@ export function unlockQuest(storyId, questId, callback) { if (!activeQuests[storyId]) { activeQuests[storyId] = {}; } - if (typeof Storage !== 'undefined' && localStorage.hasOwnProperty(questId)) { + if (typeof Storage !== 'undefined' && GameState.hasCompletedQuest(questId)) { // skip sequence skipEffects(quest.children[0]); SaveManager.saveUnlockQuest(storyId, questId); From 9aa15e031af9c4d1f4a92d190759361cf24e7753 Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 16 Apr 2020 04:26:51 +0800 Subject: [PATCH 22/39] Save Manager now saves story action sequence and start location to localStorage --- src/components/academy/game/constants/constants.js | 2 ++ .../academy/game/save-manager/save-manager.js | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/academy/game/constants/constants.js b/src/components/academy/game/constants/constants.js index db1135a92b..d4c3b09bf4 100644 --- a/src/components/academy/game/constants/constants.js +++ b/src/components/academy/game/constants/constants.js @@ -24,5 +24,7 @@ export const avatarPath = ASSETS_HOST + 'avatars/', uiPath = ASSETS_HOST + 'UI/', soundPath = ASSETS_HOST + 'sounds/', + saveDataKey = "source_academy_save_data", + locationKey = "source_academy_location", fadeTime = 0.3, nullFunction = function() {}; \ No newline at end of file diff --git a/src/components/academy/game/save-manager/save-manager.js b/src/components/academy/game/save-manager/save-manager.js index d67813cdb0..d741c60dc5 100644 --- a/src/components/academy/game/save-manager/save-manager.js +++ b/src/components/academy/game/save-manager/save-manager.js @@ -1,4 +1,5 @@ import {saveStudentData} from '../backend/game-state'; +import {saveDataKey} from "../constants/constants"; var LocationManager = require('../location-manager/location-manager.js'); var QuestManager = require('../quest-manager/quest-manager.js'); @@ -10,14 +11,13 @@ var Utils = require('../utils/utils.js'); var actionSequence = []; -export function init(saveData, callback) { +// finds existing save data, which consists of action sequence and starting location +export function init() { + let saveData = localStorage.getItem(saveDataKey); if (saveData) { - alert(saveData); saveData = JSON.parse(saveData); actionSequence = saveData.actionSequence; var storyXMLs = []; - // TODO: this is assuming that all 'loadStory' appear at the start - // This may not be the case. Need to improve for (var i = 0; i < actionSequence.length; i++) { if (actionSequence[i].type == 'loadStory') { storyXMLs.push(actionSequence[i].storyId); @@ -110,8 +110,9 @@ export function saveLoadStories(stories) { saveGame(); } +// saves actionsequence and start location into local storage function saveGame() { - saveStudentData( + localStorage.setItem(saveDataKey, JSON.stringify({ actionSequence: actionSequence, startLocation: LocationManager.getStartLocation() From 6a3e61d5384732670cd0433cd7cab9b844da43db Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 16 Apr 2020 04:31:23 +0800 Subject: [PATCH 23/39] Added new actions and changed state for the addition of game state. - Made new actiontypes to update game state in the backend and in session. - Updated tests to accomodate for game state. --- src/actions/__tests__/session.ts | 5 ++-- src/actions/actionTypes.ts | 2 ++ src/actions/game.ts | 5 +++- src/actions/material.ts | 2 -- src/actions/session.ts | 13 +++++++--- src/mocks/backend.ts | 5 ++-- src/reducers/__tests__/session.ts | 9 +++++-- src/reducers/session.ts | 6 +++++ src/reducers/states.ts | 16 +++++++++++- src/sagas/__tests__/backend.ts | 24 ++++++++++++++---- src/sagas/backend.ts | 42 +++++++++++++------------------ src/sagas/requests.ts | 19 ++++++++++++++ 12 files changed, 105 insertions(+), 43 deletions(-) diff --git a/src/actions/__tests__/session.ts b/src/actions/__tests__/session.ts index 47e0c12e0f..eda44a8db6 100644 --- a/src/actions/__tests__/session.ts +++ b/src/actions/__tests__/session.ts @@ -1,7 +1,7 @@ import { Grading, GradingOverview } from '../../components/academy/grading/gradingShape'; import { IAssessment, IAssessmentOverview } from '../../components/assessment/assessmentShape'; import { Notification } from '../../components/notification/notificationShape'; -import { Role, Story } from '../../reducers/states'; +import { GameState, Role, Story } from '../../reducers/states'; import * as actionTypes from '../actionTypes'; import { acknowledgeNotifications, @@ -155,7 +155,8 @@ test('setUser generates correct action object', () => { name: 'test student', role: 'student' as Role, grade: 150, - story: {} as Story + story: {} as Story, + gameState: {} as GameState }; const action = setUser(user); expect(action).toEqual({ diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index 2ef04053b8..ad4e704f7e 100755 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -124,3 +124,5 @@ export const NOTIFY_CHATKIT_USERS = 'NOTIFY_CHATKIT_USERS'; /** GAMEDEV */ export const FETCH_TEST_STORIES = 'FETCH_TEST_STORIES'; +export const SAVE_USER_STATE = 'SAVE_USER_STATE'; +export const SET_GAME_STATE = 'SET_GAME_STATE'; diff --git a/src/actions/game.ts b/src/actions/game.ts index c552f98a12..22fce7c6bc 100644 --- a/src/actions/game.ts +++ b/src/actions/game.ts @@ -1,5 +1,8 @@ +import { GameState } from 'src/reducers/states'; import { action } from 'typesafe-actions'; - import * as actionTypes from './actionTypes'; +export const fetchTestStories = () => action(actionTypes.FETCH_TEST_STORIES); export const saveCanvas = (canvas: HTMLCanvasElement) => action(actionTypes.SAVE_CANVAS, canvas); +export const saveUserData = (gameState: GameState) => + action(actionTypes.SAVE_USER_STATE, gameState); diff --git a/src/actions/material.ts b/src/actions/material.ts index 487b029e72..c766bf9585 100644 --- a/src/actions/material.ts +++ b/src/actions/material.ts @@ -21,5 +21,3 @@ export const updateMaterialIndex = (index: MaterialData[]) => export const uploadMaterial = (file: File, title: string, description: string) => action(actionTypes.UPLOAD_MATERIAL, { file, title, description }); - -export const fetchTestStories = () => action(actionTypes.FETCH_TEST_STORIES); diff --git a/src/actions/session.ts b/src/actions/session.ts index 73307c76e1..aa5161a701 100755 --- a/src/actions/session.ts +++ b/src/actions/session.ts @@ -6,7 +6,7 @@ import { Notification, NotificationFilterFunction } from '../components/notification/notificationShape'; -import { Story } from '../reducers/states'; +import { GameState, Story } from '../reducers/states'; import * as actionTypes from './actionTypes'; import { Role } from '../reducers/states'; @@ -31,6 +31,8 @@ export const fetchGradingOverviews = (filterToGroup = true) => export const login = () => action(actionTypes.LOGIN); +export const setGameState = (gameState: GameState) => action(actionTypes.SET_GAME_STATE, gameState); + export const setTokens = ({ accessToken, refreshToken @@ -43,8 +45,13 @@ export const setTokens = ({ refreshToken }); -export const setUser = (user: { name: string; role: Role; grade: number; story: Story }) => - action(actionTypes.SET_USER, user); +export const setUser = (user: { + name: string; + role: Role; + grade: number; + story?: Story; + gameState?: GameState; +}) => action(actionTypes.SET_USER, user); export const submitAnswer = (id: number, answer: string | number) => action(actionTypes.SUBMIT_ANSWER, { diff --git a/src/mocks/backend.ts b/src/mocks/backend.ts index a94ee8a80d..3750557e97 100644 --- a/src/mocks/backend.ts +++ b/src/mocks/backend.ts @@ -15,7 +15,7 @@ import { NotificationFilterFunction } from '../components/notification/notificationShape'; import { store } from '../createStore'; -import { IState, Role } from '../reducers/states'; +import { GameState, IState, Role } from '../reducers/states'; import { history } from '../utils/history'; import { showSuccessMessage, showWarningMessage } from '../utils/notification'; import { mockAssessmentOverviews, mockAssessments } from './assessmentAPI'; @@ -35,7 +35,8 @@ export function* mockBackendSaga(): SagaIterator { story: 'mission-1', playStory: true }, - grade: 0 + grade: 0, + gameState: {} as GameState }; store.dispatch(actions.setTokens(tokens)); store.dispatch(actions.setUser(user)); diff --git a/src/reducers/__tests__/session.ts b/src/reducers/__tests__/session.ts index 6003431ebe..615bdf1056 100644 --- a/src/reducers/__tests__/session.ts +++ b/src/reducers/__tests__/session.ts @@ -20,7 +20,7 @@ import { import { Notification } from '../../components/notification/notificationShape'; import { HistoryHelper } from '../../utils/history'; import { reducer } from '../session'; -import { defaultSession, ISessionState, Role, Story } from '../states'; +import { defaultSession, GameState, ISessionState, Role, Story } from '../states'; test('LOG_OUT works correctly on default session', () => { const action = { @@ -56,11 +56,16 @@ test('SET_USER works correctly', () => { story: 'test story', playStory: true }; + const gameState: GameState = { + collectibles: {}, + completed_quests: [] + }; const payload = { name: 'test student', role: Role.Student, grade: 150, - story + story, + gameState }; const action = { diff --git a/src/reducers/session.ts b/src/reducers/session.ts index eb8f510355..2a4c72abfe 100644 --- a/src/reducers/session.ts +++ b/src/reducers/session.ts @@ -4,6 +4,7 @@ import { ActionType } from 'typesafe-actions'; import * as actions from '../actions'; import { LOG_OUT, + SET_GAME_STATE, SET_TOKENS, SET_USER, UPDATE_ASSESSMENT, @@ -88,6 +89,11 @@ export const reducer: Reducer = ( ...state, notifications: action.payload }; + case SET_GAME_STATE: + return { + ...state, + gameState: action.payload + }; default: return state; } diff --git a/src/reducers/states.ts b/src/reducers/states.ts index 1ed4f99bde..b7816dd588 100755 --- a/src/reducers/states.ts +++ b/src/reducers/states.ts @@ -138,7 +138,8 @@ export interface ISessionState { readonly maxXp: number; readonly refreshToken?: string; readonly role?: Role; - readonly story?: Story; + readonly story: Story; + readonly gameState: GameState; readonly name?: string; readonly xp: number; readonly notifications: Notification[]; @@ -157,6 +158,11 @@ export type Story = { playStory: boolean; }; +export type GameState = { + collectibles: { [id: string]: string }; + completed_quests: string[]; +}; + /** * An output while the program is still being run in the interpreter. As a * result, there are no return values or SourceErrors yet. However, there could @@ -423,6 +429,14 @@ export const defaultSession: ISessionState = { refreshToken: undefined, role: undefined, name: undefined, + story: { + story: '', + playStory: false + }, + gameState: { + completed_quests: [], + collectibles: {} + }, xp: 0, notifications: [] }; diff --git a/src/sagas/__tests__/backend.ts b/src/sagas/__tests__/backend.ts index 3fd42c1d30..0a81fc7309 100644 --- a/src/sagas/__tests__/backend.ts +++ b/src/sagas/__tests__/backend.ts @@ -16,7 +16,7 @@ import { mockAssessments } from '../../mocks/assessmentAPI'; import { mockNotifications } from '../../mocks/userAPI'; -import { Role, Story } from '../../reducers/states'; +import { GameState, Role, Story } from '../../reducers/states'; import { showSuccessMessage, showWarningMessage } from '../../utils/notification'; import backendSaga from '../backend'; import { @@ -67,8 +67,15 @@ describe('Test FETCH_AUTH Action', () => { const user = { name: 'user', role: 'student' as Role, - story: {} as Story, - grade: 1 + story: { + story: '', + playStory: false + } as Story, + grade: 1, + gameState: { + collectibles: {}, + completed_quests: [] + } as GameState }; return expectSaga(backendSaga) .call(postAuth, luminusCode) @@ -85,8 +92,15 @@ describe('Test FETCH_AUTH Action', () => { const user = { name: 'user', role: 'student' as Role, - story: {} as Story, - grade: 1 + story: { + story: '', + playStory: false + } as Story, + grade: 1, + gameState: { + collectibles: {}, + completed_quests: [] + } as GameState }; return expectSaga(backendSaga) .provide([[call(postAuth, luminusCode), null], [call(getUser, mockTokens), user]]) diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index 43f4447981..5240c8d4cd 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -22,7 +22,7 @@ import { Notification, NotificationFilterFunction } from '../components/notification/notificationShape'; -import { IState, Role } from '../reducers/states'; +import { GameState, IState, Role } from '../reducers/states'; import { history } from '../utils/history'; import { showSuccessMessage, showWarningMessage } from '../utils/notification'; import * as request from './requests'; @@ -561,30 +561,6 @@ function* backendSaga(): SagaIterator { action: ReturnType ) { const fileName: string = 'Test Stories'; - /* - yield put(actions.fetchMaterialIndex()); - let materialIndex = null; - while (materialIndex === null) { - materialIndex = yield select( - (state: IState) => state.session.materialIndex! - ); - } - let storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); - - if (storyFolder === undefined) { - yield put(actions.createMaterialFolder(fileName)); - while (yield materialIndex.find((x :MaterialData) => x.title === fileName) === undefined) { - materialIndex = yield select( - (state: IState) => state.session.materialIndex! - ); - } - storyFolder = yield materialIndex.find((x :MaterialData) => x.title === fileName); - } - // tslint:disable-next-line:no-console - console.log(storyFolder.id == null ? 1 : 0); - yield put(actions.fetchMaterialIndex(storyFolder.id)); - */ - const tokens = yield select((state: IState) => ({ accessToken: state.session.accessToken, refreshToken: state.session.refreshToken @@ -619,6 +595,22 @@ function* backendSaga(): SagaIterator { } } }); + + yield takeEvery(actionTypes.SAVE_USER_STATE, function*( + action: ReturnType + ) { + const tokens = yield select((state: IState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + const gameState: GameState = action.payload; + const resp = yield request.putUserGameState(gameState, tokens); + if (!resp || !resp.ok) { + yield request.handleResponseError(resp); + return; + } + yield put(actions.setGameState(gameState)); + }); } export default backendSaga; diff --git a/src/sagas/requests.ts b/src/sagas/requests.ts index 9899982ab6..4c4c1e89f4 100644 --- a/src/sagas/requests.ts +++ b/src/sagas/requests.ts @@ -2,6 +2,7 @@ /*eslint-env browser*/ import { call } from 'redux-saga/effects'; +import { GameState } from 'src/reducers/states'; import * as actions from '../actions'; import { Grading, @@ -101,6 +102,24 @@ export async function getUser(tokens: Tokens): Promise { return await resp.json(); } +/** + * PUT /user/game_states/ + */ +export async function putUserGameState( + // eslint-disable-next-line + gameStates: GameState, + tokens: Tokens +): Promise { + const resp = await request('user/game_states/save', 'PUT', { + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + body: { + gameStates: JSON.stringify(gameStates) + } + }); + return resp; +} + /** * GET /assessments */ From 12f5c98363eff3443025ab3e7844dae501476397 Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 16 Apr 2020 04:33:45 +0800 Subject: [PATCH 24/39] Updated game and gamedev components to accomodate the addition of game state and changes in save manager. --- .../academy/game/create-initializer.js | 14 +++++------ src/components/academy/game/game.js | 4 ++-- src/components/academy/game/index.tsx | 23 ++++++++----------- .../academy/game/story-xml-player.js | 7 +++--- src/components/game-dev/JsonUpload.tsx | 9 ++++---- src/components/game-dev/StoryTable.tsx | 4 ++-- src/containers/GameContainer.ts | 9 +++++--- src/sagas/requests.ts | 1 - 8 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/components/academy/game/create-initializer.js b/src/components/academy/game/create-initializer.js index 6234fef762..2071d82730 100644 --- a/src/components/academy/game/create-initializer.js +++ b/src/components/academy/game/create-initializer.js @@ -1,9 +1,9 @@ import {LINKS} from '../../../utils/constants' import {history} from '../../../utils/history' import {soundPath} from './constants/constants' -import {fetchGameData, getMissionPointer, getStudentData, saveCollectible, saveQuest, saveStudentData} from './backend/game-state' +import {fetchGameData, getMissionPointer, getStudentData, saveCollectible, saveQuest} from './backend/game-state' -export default function (StoryXMLPlayer, username, userStory) { +export default function (StoryXMLPlayer, username, userStory, gameState) { var hookHandlers = { startMission: function () { @@ -50,9 +50,8 @@ export default function (StoryXMLPlayer, username, userStory) { window.open(LINKS.LUMINUS); } - function startGame(div, canvas, saveData) { + function startGame(div, canvas) { StoryXMLPlayer.init(div, canvas, { - saveData: saveData, hookHandlers: hookHandlers, wristDeviceFunc: openWristDevice, playerName: username, @@ -60,16 +59,17 @@ export default function (StoryXMLPlayer, username, userStory) { changeLocationHook: function (newLocation) { if (typeof Storage !== 'undefined') { // Code for localStorage/sessionStorage. - localStorage.cs1101s_source_academy_location = newLocation; + localStorage.locationKey = newLocation; } } }); } function initialize(div, canvas) { - startGame(div, canvas, getStudentData()); + + startGame(div, canvas); StoryXMLPlayer.loadStory(getMissionPointer(), function () {}); } - return (div, canvas) => fetchGameData(userStory, () => initialize(div, canvas)); + return (div, canvas) => fetchGameData(userStory, gameState, () => initialize(div, canvas)); }; diff --git a/src/components/academy/game/game.js b/src/components/academy/game/game.js index f08ba9bb29..9e47693710 100644 --- a/src/components/academy/game/game.js +++ b/src/components/academy/game/game.js @@ -1,8 +1,8 @@ import createInitializer from './create-initializer' -export default function(div, canvas, username, userStory) { +export default function(div, canvas, username, userStory, gameState) { var StoryXMLPlayer = require('./story-xml-player'); var container = document.getElementById('game-display') - var initialize = createInitializer(StoryXMLPlayer, username, userStory) + var initialize = createInitializer(StoryXMLPlayer, username, userStory, gameState) initialize(div, canvas); } diff --git a/src/components/academy/game/index.tsx b/src/components/academy/game/index.tsx index 8b7ba94194..8ca617aeb0 100644 --- a/src/components/academy/game/index.tsx +++ b/src/components/academy/game/index.tsx @@ -1,19 +1,21 @@ import * as React from 'react'; - -import { store } from '../../../createStore'; -import { Story } from '../../../reducers/states'; +import { GameState, Role, Story } from '../../../reducers/states'; +import { setSaveHandler } from './backend/game-state'; import { setUserRole } from './backend/user'; type GameProps = DispatchProps & StateProps; export type DispatchProps = { handleSaveCanvas: (c: HTMLCanvasElement) => void; + handleSaveData: (s: GameState) => void; }; export type StateProps = { canvas?: HTMLCanvasElement; name: string; - story?: Story; + story: Story; + gameState: GameState; + role?: Role; }; export class Game extends React.Component { @@ -39,8 +41,10 @@ export class Game extends React.Component { public async componentDidMount() { const story: any = (await import('./game.js')).default; if (this.props.canvas === undefined) { - const storyOpts = await this.getStoryOpts(); - story(this.div, this.canvas, this.props.name, storyOpts); + setUserRole(this.props.role); + setSaveHandler((gameState: GameState) => this.props.handleSaveData(gameState)); + + story(this.div, this.canvas, this.props.name, this.props.story, this.props.gameState); this.props.handleSaveCanvas(this.canvas); } else { // This browser window has loaded the Game component & canvas before @@ -56,13 +60,6 @@ export class Game extends React.Component { ); } - - private async getStoryOpts() { - const defaultStory = { story: 10, playStory: true }; - const userStory = this.props.story ? this.props.story : store.getState().session.story; - setUserRole(store.getState().session.role); - return userStory ? userStory : defaultStory; - } } export default Game; diff --git a/src/components/academy/game/story-xml-player.js b/src/components/academy/game/story-xml-player.js index 09a080891a..f02569ce2d 100644 --- a/src/components/academy/game/story-xml-player.js +++ b/src/components/academy/game/story-xml-player.js @@ -20,9 +20,10 @@ var stage; // options contains the following properties: // saveData, hookHandlers, wristDeviceFunc // changeLocationHook, playerImageCanvas, playerName -export function init(div, canvas, options, callback) { +export function init(div, canvas, options) { renderer = PIXI.autoDetectRenderer( Constants.screenWidth, + Constants.screenHeight, { backgroundColor: 0x000000, view: canvas } ); @@ -51,8 +52,8 @@ export function init(div, canvas, options, callback) { } animate(); - SaveManager.init(options.saveData, callback); - + SaveManager.init(); + // a pixi.container on top of everything that is exported stage.addChild(ExternalManager.init(options.hookHandlers)); }; diff --git a/src/components/game-dev/JsonUpload.tsx b/src/components/game-dev/JsonUpload.tsx index 7bd27b1c6a..7067880ed8 100644 --- a/src/components/game-dev/JsonUpload.tsx +++ b/src/components/game-dev/JsonUpload.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { overrideGameState } from '../academy/game/backend/game-state.js'; class JsonUpload extends React.Component { - private static onFormSubmit(e: { preventDefault: () => void; }){ + private static onFormSubmit(e: { preventDefault: () => void }) { e.preventDefault(); // Stop form submit } @@ -17,18 +17,17 @@ class JsonUpload extends React.Component { return (

Game State Override

- +
); } - private onChange(e: { target: { files: any; }; }) { + private onChange(e: { target: { files: any } }) { const reader = new FileReader(); reader.onload = (event: Event) => { - overrideGameState(JSON.parse(""+reader.result)); + overrideGameState(JSON.parse('' + reader.result)); }; reader.readAsText(e.target.files[0]); } } - export default JsonUpload; diff --git a/src/components/game-dev/StoryTable.tsx b/src/components/game-dev/StoryTable.tsx index 8c8c6aa546..990844b780 100644 --- a/src/components/game-dev/StoryTable.tsx +++ b/src/components/game-dev/StoryTable.tsx @@ -23,7 +23,7 @@ import { getStandardDateTime } from '../../utils/dateHelpers'; import { controlButton } from '../commons'; import DeleteCell from './DeleteCell'; import DownloadCell from './DownloadCell'; -import JsonUpload from "./JsonUpload"; +import JsonUpload from './JsonUpload'; import { DirectoryData, MaterialData } from './storyShape'; /** @@ -174,7 +174,7 @@ class StoryTable extends React.Component { -
+
diff --git a/src/containers/GameContainer.ts b/src/containers/GameContainer.ts index 53388adb31..940dcdf95f 100644 --- a/src/containers/GameContainer.ts +++ b/src/containers/GameContainer.ts @@ -1,14 +1,15 @@ import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { saveCanvas } from '../actions/game'; +import { saveCanvas, saveUserData } from '../actions/game'; import Game, { DispatchProps, StateProps } from '../components/academy/game'; import { IState } from '../reducers/states'; const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( { - handleSaveCanvas: saveCanvas + handleSaveCanvas: saveCanvas, + handleSaveData: saveUserData }, dispatch ); @@ -16,7 +17,9 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis const mapStateToProps: MapStateToProps = state => ({ canvas: state.academy.gameCanvas, name: state.session.name!, - story: state.session.story + story: state.session.story, + gameState: state.session.gameState, + role: state.session.role }); export default connect( diff --git a/src/sagas/requests.ts b/src/sagas/requests.ts index 4c4c1e89f4..cf047d9e5f 100644 --- a/src/sagas/requests.ts +++ b/src/sagas/requests.ts @@ -106,7 +106,6 @@ export async function getUser(tokens: Tokens): Promise { * PUT /user/game_states/ */ export async function putUserGameState( - // eslint-disable-next-line gameStates: GameState, tokens: Tokens ): Promise { From edcd243efe8a70de4ecd74b0295bdbbdfa9c3502 Mon Sep 17 00:00:00 2001 From: travisryte Date: Tue, 21 Apr 2020 15:55:38 +0800 Subject: [PATCH 25/39] Deleted non-essential mission pointer override --- src/components/academy/game/backend/game-state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 50e8540dee..14f766bec6 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -39,7 +39,7 @@ export function overrideGameState(data) { studentStoryOverride = data.story; currentDateOverride = data.currentDate; } else { - studentStoryOverride = studentDataOverride = missionPointerOverride = currentDateOverride = undefined; + studentStoryOverride = studentDataOverride = currentDateOverride = undefined; } } From d023ede27f3a7d732ce4528b9b14eee15007f03d Mon Sep 17 00:00:00 2001 From: travisryte Date: Tue, 21 Apr 2020 19:31:51 +0800 Subject: [PATCH 26/39] Fixed Typo 'hasColletible'. --- src/components/academy/game/object-manager/object-manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/academy/game/object-manager/object-manager.js b/src/components/academy/game/object-manager/object-manager.js index 6fdcf6f580..8c6b61e908 100644 --- a/src/components/academy/game/object-manager/object-manager.js +++ b/src/components/academy/game/object-manager/object-manager.js @@ -165,8 +165,8 @@ export function processTempObject(gameLocation, node) { } var collectible = node.getAttribute('name'); var isInDorm = gameLocation.name == 'yourRoom'; - if ((isInDorm && !GameState.hasColletible(collectible))|| - (!isInDorm && GameState.hasColletible(collectible))) { + if ((isInDorm && !GameState.hasCollectible(collectible))|| + (!isInDorm && GameState.hasCollectible(collectible))) { return; //don't load the collectible in dorm if it's not collected || don't load if in hidden location and collected } if (isInDorm) { From 3c5579b3bf31d43e2317b2e75f3ceaf7ed5e9684 Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 00:18:53 +0800 Subject: [PATCH 27/39] Merged GroundControl with gameStateUpdate --- package-lock.json | 565 +++++++++++++++--- package.json | 1 + src/actions/__tests__/groundControl.ts | 57 ++ src/actions/actionTypes.ts | 6 + src/actions/groundControl.ts | 14 + src/actions/index.ts | 1 + src/components/Playground.tsx | 0 src/components/academy/NavigationBar.tsx | 9 + .../__snapshots__/NavigationBar.tsx.snap | 12 + src/components/academy/index.tsx | 2 + src/components/assessment/assessmentShape.ts | 1 + src/components/groundControl/DeleteCell.tsx | 60 ++ src/components/groundControl/Dropzone.tsx | 151 +++++ src/components/groundControl/EditCell.tsx | 103 ++++ .../groundControl/GroundControl.tsx | 230 +++++++ src/components/groundControl/PublishCell.tsx | 70 +++ src/components/workspace/Editor.tsx | 2 + src/containers/PlaygroundContainer.ts | 0 .../groundControl/GroundControlContainer.ts | 36 ++ src/reducers/states.ts | 0 src/sagas/backend.ts | 89 ++- src/sagas/requests.ts | 56 ++ src/styles/_groundcontrol.scss | 7 + src/styles/index.scss | 2 + src/utils/constants.ts | 0 25 files changed, 1393 insertions(+), 81 deletions(-) create mode 100644 src/actions/__tests__/groundControl.ts create mode 100644 src/actions/groundControl.ts mode change 100755 => 100644 src/components/Playground.tsx create mode 100644 src/components/groundControl/DeleteCell.tsx create mode 100644 src/components/groundControl/Dropzone.tsx create mode 100644 src/components/groundControl/EditCell.tsx create mode 100644 src/components/groundControl/GroundControl.tsx create mode 100644 src/components/groundControl/PublishCell.tsx mode change 100755 => 100644 src/components/workspace/Editor.tsx mode change 100755 => 100644 src/containers/PlaygroundContainer.ts create mode 100644 src/containers/groundControl/GroundControlContainer.ts mode change 100755 => 100644 src/reducers/states.ts create mode 100644 src/styles/_groundcontrol.scss mode change 100755 => 100644 src/utils/constants.ts diff --git a/package-lock.json b/package-lock.json index 893ac6e23b..a74c408fbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -157,6 +157,155 @@ } } }, + "@blueprintjs/datetime": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/@blueprintjs/datetime/-/datetime-3.15.2.tgz", + "integrity": "sha512-FQw1BqbO9RBKzLWiXHkSVFxyGFRXHaugG5ST4go+p2IibrxuRDjD6YvrFXo+FLEzi+MsftMo6FkPNm2xApfmHw==", + "requires": { + "@blueprintjs/core": "^3.23.0", + "classnames": "^2.2", + "react-day-picker": "7.3.2", + "tslib": "~1.9.0" + }, + "dependencies": { + "@blueprintjs/core": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.24.0.tgz", + "integrity": "sha512-qW29DDPjzYsT27J6n97C0jZ1ifvEEziwNC98UhaKdSE7I8qxbLsb+ft2JOop+pEX4ab67T1lhQKAiQjWCPKZng==", + "requires": { + "@blueprintjs/icons": "^3.14.0", + "@types/dom4": "^2.0.1", + "classnames": "^2.2", + "dom4": "^2.1.5", + "normalize.css": "^8.0.1", + "popper.js": "^1.15.0", + "react-lifecycles-compat": "^3.0.4", + "react-popper": "^1.3.7", + "react-transition-group": "^2.9.0", + "resize-observer-polyfill": "^1.5.1", + "tslib": "~1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + } + } + }, + "@blueprintjs/icons": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.14.0.tgz", + "integrity": "sha512-cvQ3CSdy0DqVqcXcPqSxoycJw497TVP5goyE6xCFlVs84477ahxh7Uung6J+CCoDVBuI87h576LtuyjwSxorvQ==", + "requires": { + "classnames": "^2.2", + "tslib": "~1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + } + } + }, + "create-react-context": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", + "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" + }, + "dependencies": { + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, + "normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.3.0", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + } + }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + } + } + }, "@blueprintjs/icons": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.9.1.tgz", @@ -897,8 +1046,7 @@ "@types/dom4": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz", - "integrity": "sha512-kSkVAvWmMZiCYtvqjqQEwOmvKwcH+V4uiv3qPQ8pAh1Xl39xggGEo8gHUqV4waYGHezdFw0rKBR8Jt0CrQSDZA==", - "dev": true + "integrity": "sha512-kSkVAvWmMZiCYtvqjqQEwOmvKwcH+V4uiv3qPQ8pAh1Xl39xggGEo8gHUqV4waYGHezdFw0rKBR8Jt0CrQSDZA==" }, "@types/dotenv": { "version": "6.1.1", @@ -5317,8 +5465,7 @@ "dom4": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/dom4/-/dom4-2.1.5.tgz", - "integrity": "sha512-gJbnVGq5zaBUY0lUh0LUEVGYrtN75Ks8ZwpwOYvnVFrKy/qzXK4R/1WuLIFExWj/tBxbRAkTzZUGJHXmqsBNjQ==", - "dev": true + "integrity": "sha512-gJbnVGq5zaBUY0lUh0LUEVGYrtN75Ks8ZwpwOYvnVFrKy/qzXK4R/1WuLIFExWj/tBxbRAkTzZUGJHXmqsBNjQ==" }, "domain-browser": { "version": "1.2.0", @@ -7904,8 +8051,7 @@ "gud": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", - "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==", - "dev": true + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" }, "gzip-size": { "version": "3.0.0", @@ -7994,8 +8140,7 @@ "has-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, "has-unicode": { "version": "2.0.1", @@ -8977,8 +9122,7 @@ "is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" }, "is-arrayish": { "version": "0.2.1", @@ -9510,7 +9654,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-22.4.2.tgz", "integrity": "sha512-wD7dXWtfaQAgbNVsjFqzmuhg6nzwGsTRVea3FpSJ7GURhG+J536fw4mdoLB01DgiEozDDeF1ZMR/UlUszTsCrg==", "dev": true, - "setupFiles": ["jest-canvas-mock"], "requires": { "import-local": "^1.0.0", "jest-cli": "^22.4.2" @@ -10822,8 +10965,8 @@ }, "js-slang": { "version": "0.4.45", - "resolved": "http://r.cnpmjs.org/js-slang/download/js-slang-0.4.45.tgz", - "integrity": "sha1-2R86KTrYXp0Zic20FyGMTqGjrhs=", + "resolved": "https://registry.npmjs.org/js-slang/-/js-slang-0.4.45.tgz", + "integrity": "sha512-sAwGrC/9QJo6GTxJbLiPYfc8NigLX43mNzStyyw3+Lno7Ww20MhDPutHrkQYWEC9n03N7n1WRspVi5vh0aknUw==", "requires": { "@types/estree": "0.0.39", "@types/lodash.assignin": "^4.2.6", @@ -10843,13 +10986,13 @@ "dependencies": { "ace-builds": { "version": "1.4.11", - "resolved": "http://r.cnpmjs.org/ace-builds/download/ace-builds-1.4.11.tgz", - "integrity": "sha1-sfGaiRr87x0mUiRzCCuvgAZ+hV8=" + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.11.tgz", + "integrity": "sha512-keACH1d7MvAh72fE/us36WQzOFQPJbHphNpj33pXwVZOM84pTWcdFzIAvngxOGIGLTm7gtUP2eJ4Ku6VaPo8bw==" }, "acorn": { "version": "6.4.1", - "resolved": "http://r.cnpmjs.org/acorn/download/acorn-6.4.1.tgz", - "integrity": "sha1-Ux5Yuj9RudrLmmZGyk3r9bFMpHQ=" + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" } } }, @@ -15542,8 +15685,7 @@ "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=" }, "object-keys": { "version": "1.0.12", @@ -15562,7 +15704,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -16321,8 +16462,7 @@ "popper.js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "dev": true + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, "portfinder": { "version": "1.0.16", @@ -18161,6 +18301,31 @@ "prop-types": "^15.5.8" } }, + "react-day-picker": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-7.3.2.tgz", + "integrity": "sha512-mij2j2Un/v2V2ow+hf/hFBMdl6Eis/C/YhBtlI6Xpbvh3Q6WMix78zEkCdw6i9GldafOrpnupWKofv/h5oSI4g==", + "requires": { + "prop-types": "^15.6.2" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "react-dev-utils": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.1.tgz", @@ -18832,7 +18997,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" @@ -18842,7 +19006,6 @@ "version": "1.17.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -18861,7 +19024,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -18871,20 +19033,17 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -18893,7 +19052,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -18901,14 +19059,12 @@ "object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" } } }, @@ -19154,8 +19310,7 @@ "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "resolve": { "version": "1.6.0", @@ -20471,37 +20626,6 @@ "version": "13.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.0.tgz", "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==" - }, - "ace-builds": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.11.tgz", - "integrity": "sha512-keACH1d7MvAh72fE/us36WQzOFQPJbHphNpj33pXwVZOM84pTWcdFzIAvngxOGIGLTm7gtUP2eJ4Ku6VaPo8bw==" - }, - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" - }, - "js-slang": { - "version": "0.4.42", - "resolved": "https://registry.npmjs.org/js-slang/-/js-slang-0.4.42.tgz", - "integrity": "sha512-jtF3Ayp03/axNC0Ez7uqOhJcpsRvGC3XtjOPa3PawPuBX1mhxMBEpAMxTAov8xz04fwGzKP12DxNxVpm1bktvQ==", - "requires": { - "@types/estree": "0.0.39", - "@types/lodash.assignin": "^4.2.6", - "@types/lodash.clonedeep": "^4.5.6", - "ace-builds": "^1.4.9", - "acorn": "^6.4.1", - "acorn-loose": "^7.0.0", - "acorn-walk": "^7.0.0", - "astring": "^1.3.1", - "gpu.js": "^2.9.3", - "jest-html-reporter": "^2.8.2", - "lodash": "^4.17.13", - "node-getopt": "^0.3.2", - "source-map": "^0.7.3", - "xmlhttprequest-ts": "^1.0.1" - } } } }, @@ -20845,24 +20969,306 @@ } } }, + "string.prototype.trimend": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz", + "integrity": "sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } } }, "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz", + "integrity": "sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } } }, "string_decoder": { @@ -21750,8 +22156,7 @@ "typed-styles": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", - "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==", - "dev": true + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" }, "typedarray": { "version": "0.0.6", diff --git a/package.json b/package.json index cd067ec87e..dfaf4e2252 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ } }, "dependencies": { + "@blueprintjs/datetime": "^3.15.2", "@pusher/chatkit-client": "^1.5.0", "ace-builds": "^1.4.8", "acorn": "^5.7.4", diff --git a/src/actions/__tests__/groundControl.ts b/src/actions/__tests__/groundControl.ts new file mode 100644 index 0000000000..be02d7d3ab --- /dev/null +++ b/src/actions/__tests__/groundControl.ts @@ -0,0 +1,57 @@ +import * as actionTypes from '../actionTypes'; +import { + changeDateAssessment, + deleteAssessment, + publishAssessment, + uploadAssessment +} from '../groundControl'; + +test('changeDateAssessment generates correct action object', () => { + const id = 10; + const openAt = '2020-01-01T00:00:00.000Z'; + const closeAt = '2021-01-01T00:00:00.000Z'; + const action = changeDateAssessment(id, openAt, closeAt); + expect(action).toEqual({ + type: actionTypes.CHANGE_DATE_ASSESSMENT, + payload: { + id, + openAt, + closeAt + } + }); +}); + +test('deleteAssessment generates correct action object', () => { + const id = 12; + const action = deleteAssessment(id); + expect(action).toEqual({ + type: actionTypes.DELETE_ASSESSMENT, + payload: id + }); +}); + +test('publishAssessment generates correct action object', () => { + const id = 54; + const togglePublishTo = false; + const action = publishAssessment(togglePublishTo, id); + expect(action).toEqual({ + type: actionTypes.PUBLISH_ASSESSMENT, + payload: { + togglePublishTo, + id + } + }); +}); + +test(' generates correct action object', () => { + const file = new File([''], 'testFile'); + const forceUpdate = true; + const action = uploadAssessment(file, forceUpdate); + expect(action).toEqual({ + type: actionTypes.UPLOAD_ASSESSMENT, + payload: { + file, + forceUpdate + } + }); +}); diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index ad4e704f7e..c8a1e4ae3d 100755 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -126,3 +126,9 @@ export const NOTIFY_CHATKIT_USERS = 'NOTIFY_CHATKIT_USERS'; export const FETCH_TEST_STORIES = 'FETCH_TEST_STORIES'; export const SAVE_USER_STATE = 'SAVE_USER_STATE'; export const SET_GAME_STATE = 'SET_GAME_STATE'; +/** GroundControl */ + +export const CHANGE_DATE_ASSESSMENT = 'CHANGE_DATE_ASSESSMENT'; +export const DELETE_ASSESSMENT = 'DELETE_ASSESSMENT'; +export const PUBLISH_ASSESSMENT = 'PUBLISH_ASSESSMENT'; +export const UPLOAD_ASSESSMENT = 'UPLOAD_ASSESSMENT'; diff --git a/src/actions/groundControl.ts b/src/actions/groundControl.ts new file mode 100644 index 0000000000..87e548ad99 --- /dev/null +++ b/src/actions/groundControl.ts @@ -0,0 +1,14 @@ +import { action } from 'typesafe-actions'; + +import * as actionTypes from './actionTypes'; + +export const changeDateAssessment = (id: number, openAt: string, closeAt: string) => + action(actionTypes.CHANGE_DATE_ASSESSMENT, { id, openAt, closeAt }); + +export const deleteAssessment = (id: number) => action(actionTypes.DELETE_ASSESSMENT, id); + +export const publishAssessment = (togglePublishTo: boolean, id: number) => + action(actionTypes.PUBLISH_ASSESSMENT, { id, togglePublishTo }); + +export const uploadAssessment = (file: File, forceUpdate: boolean) => + action(actionTypes.UPLOAD_ASSESSMENT, { file, forceUpdate }); diff --git a/src/actions/index.ts b/src/actions/index.ts index 4f37fef163..a24f66eba4 100755 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,6 +1,7 @@ export * from './collabEditing'; export * from './commons'; export * from './game'; +export * from './groundControl'; export * from './interpreter'; export * from './material'; export * from './playground'; diff --git a/src/components/Playground.tsx b/src/components/Playground.tsx old mode 100755 new mode 100644 diff --git a/src/components/academy/NavigationBar.tsx b/src/components/academy/NavigationBar.tsx index ce53170375..6ddd614cf9 100644 --- a/src/components/academy/NavigationBar.tsx +++ b/src/components/academy/NavigationBar.tsx @@ -81,6 +81,15 @@ const NavigationBar: React.SFC = props => ( {props.role === Role.Admin || props.role === Role.Staff ? ( + + +
Ground Control
+
+ + + +
+ Ground Control +
+
@@ -148,6 +154,12 @@ exports[`Grading NavLink renders for Role.Staff 1`] = ` + + +
+ Ground Control +
+
diff --git a/src/components/academy/index.tsx b/src/components/academy/index.tsx index 4a69c65955..872548ccb5 100644 --- a/src/components/academy/index.tsx +++ b/src/components/academy/index.tsx @@ -5,6 +5,7 @@ import Grading from '../../containers/academy/grading'; import AssessmentContainer from '../../containers/assessment'; import StoryUpload from '../../containers/game-dev/StoryUploadContainer'; import Game from '../../containers/GameContainer'; +import GroundControl from '../../containers/groundControl/GroundControlContainer'; import MaterialUpload from '../../containers/material/MaterialUploadContainer'; import Sourcereel from '../../containers/sourcecast/SourcereelContainer'; import { isAcademyRe } from '../../reducers/session'; @@ -78,6 +79,7 @@ class Academy extends React.Component { render={assessmentRenderFactory(AssessmentCategories.Practical)} /> + diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index ef801ba028..a46541dec7 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -27,6 +27,7 @@ export interface IAssessmentOverview { xp: number; gradingStatus: GradingStatus; private?: boolean; + isPublished?: boolean; } export enum AssessmentStatuses { diff --git a/src/components/groundControl/DeleteCell.tsx b/src/components/groundControl/DeleteCell.tsx new file mode 100644 index 0000000000..0605614ed6 --- /dev/null +++ b/src/components/groundControl/DeleteCell.tsx @@ -0,0 +1,60 @@ +import { Classes, Dialog } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; + +import { IAssessmentOverview } from '../assessment/assessmentShape'; +import { controlButton } from '../commons'; + +interface IDeleteCellProps { + data: IAssessmentOverview; + handleDeleteAssessment: (id: number) => void; +} + +interface IDeleteCellState { + dialogOpen: boolean; +} + +class DeleteCell extends React.Component { + public constructor(props: IDeleteCellProps) { + super(props); + this.state = { + dialogOpen: false + }; + } + + public render() { + return ( +
+ {controlButton('', IconNames.TRASH, this.handleOpenDialog)} + +
+ {

Are you sure that you want to delete this Assessment?

} + {

Students' answers and submissions will be deleted as well.

} +
+
+
+ {controlButton('Confirm Delete', IconNames.TRASH, this.handleDelete)} + {controlButton('Cancel', IconNames.CROSS, this.handleCloseDialog)} +
+
+
+
+ ); + } + + private handleCloseDialog = () => this.setState({ dialogOpen: false }); + private handleOpenDialog = () => this.setState({ dialogOpen: true }); + private handleDelete = () => { + const { data } = this.props; + this.props.handleDeleteAssessment(data.id); + this.handleCloseDialog(); + }; +} + +export default DeleteCell; diff --git a/src/components/groundControl/Dropzone.tsx b/src/components/groundControl/Dropzone.tsx new file mode 100644 index 0000000000..fc9cf91935 --- /dev/null +++ b/src/components/groundControl/Dropzone.tsx @@ -0,0 +1,151 @@ +import { Card, Elevation, Switch } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { FlexDirectionProperty } from 'csstype'; +import * as React from 'react'; +import { useDropzone } from 'react-dropzone'; + +import { controlButton } from '../commons'; + +interface IDispatchProps { + handleUploadAssessment: (file: File) => void; + toggleForceUpdate: () => void; + toggleDisplayConfirmation: () => void; +} + +interface IStateProps { + forceUpdate: boolean; + displayConfirmation: boolean; +} + +interface IDropzoneProps extends IDispatchProps, IStateProps {} + +// Dropzone styling +const dropZoneStyle = { + baseStyle: { + flex: 1, + display: 'flex', + height: '30vh', + flexDirection: 'column' as FlexDirectionProperty, + alignItems: 'center', + justifyContent: 'center', + padding: '20px', + borderWidth: 2, + borderRadius: 2, + borderColor: '#eeeeee', + borderStyle: 'dashed', + backgroundColor: '#fafafa', + color: '#bdbdbd', + outline: 'none', + transition: 'border .24s ease-in-out' + }, + + activeStyle: { + borderColor: '#2196f3' + }, + + acceptStyle: { + borderColor: '#00e676' + }, + + rejectStyle: { + borderColor: '#ff1744' + } +}; + +const MaterialDropzone: React.FC = props => { + const [file, setFile] = React.useState(); + const [title, setTitle] = React.useState(); + const handleConfirmUpload = () => { + props.handleUploadAssessment(file!); + setFile(undefined); + }; + const handleCancelUpload = () => setFile(undefined); + + const { + getRootProps, + getInputProps, + isDragActive, + isDragAccept, + isDragReject, + isFocused + } = useDropzone({ + onDrop: acceptedFiles => { + setFile(acceptedFiles[0]); + setTitle(acceptedFiles[0].name); + } + }); + const style = React.useMemo( + () => ({ + ...dropZoneStyle.baseStyle, + ...(isDragActive ? dropZoneStyle.activeStyle : {}), + ...(isDragAccept ? dropZoneStyle.acceptStyle : {}), + ...(isDragReject ? dropZoneStyle.rejectStyle : {}), + ...(isFocused ? dropZoneStyle.activeStyle : {}) + }), + [isDragActive, isDragAccept, isDragReject, isFocused] + ); + + const handleToggleOnChange = () => { + if (!props.forceUpdate) { + props.toggleDisplayConfirmation(); + props.toggleForceUpdate(); + } else { + props.toggleForceUpdate(); + } + }; + + const toggleButton = () => { + return ( +
+ +
+ ); + }; + + const handleConfirmForceUpdate = () => { + props.toggleDisplayConfirmation(); + }; + + const handleCancelForceUpdate = () => { + props.toggleDisplayConfirmation(); + props.toggleForceUpdate(); + }; + + const confirmationMessage = () => { + return ( +
+

Are you sure that you want to force update the assessment?

+ {controlButton('Yes', IconNames.CONFIRM, handleConfirmForceUpdate)} + {controlButton('No', IconNames.CROSS, handleCancelForceUpdate)} +
+ ); + }; + + return ( + <> + +
+ +

Drag 'n' drop some files here, or click to select files

+
+
+ {file && ( + +
{title}
+
+ {!props.displayConfirmation && + controlButton('Confirm Upload', IconNames.UPLOAD, handleConfirmUpload)} + {!props.displayConfirmation && + controlButton('Cancel Upload', IconNames.DELETE, handleCancelUpload)} +
+
+ {!props.displayConfirmation &&

Force update opened assessment

} + {props.displayConfirmation && confirmationMessage()} + {!props.displayConfirmation && toggleButton()} +
+ )} + + ); +}; + +export default MaterialDropzone; diff --git a/src/components/groundControl/EditCell.tsx b/src/components/groundControl/EditCell.tsx new file mode 100644 index 0000000000..ad2a91160c --- /dev/null +++ b/src/components/groundControl/EditCell.tsx @@ -0,0 +1,103 @@ +import { Classes, Dialog } from '@blueprintjs/core'; +import { DateInput } from '@blueprintjs/datetime'; +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; + +import { controlButton } from '../commons'; +import { IGroundControlAssessmentOverview } from './GroundControl'; + +interface IEditCellProps { + data: IGroundControlAssessmentOverview; + handleAssessmentChangeDate: (id: number, openAt: string, closeAt: string) => void; + forOpenDate: boolean; +} + +interface IEditCellState extends IEditCellDateState { + dialogOpen: boolean; +} + +interface IEditCellDateState { + openAt: Date; + closeAt: Date; +} + +class EditCell extends React.Component { + private maxDate = new Date(new Date(Date.now()).setFullYear(2100)); + + public constructor(props: IEditCellProps) { + super(props); + this.state = { + dialogOpen: false, + openAt: new Date(Date.parse(this.props.data.openAt)), + closeAt: new Date(Date.parse(this.props.data.closeAt)) + }; + } + + public render() { + const fieldName = this.props.forOpenDate ? 'Opening' : 'Closing'; + return ( +
+ {this.props.forOpenDate ? this.props.data.prettyOpenAt : this.props.data.prettyCloseAt} + {controlButton('', IconNames.EDIT, this.handleOpenDialog)} + +
+ {fieldName} Date: {this.dateInput()} +
+
+
+ {controlButton('Confirm Update', IconNames.TICK, this.handleUpdate)} + {controlButton('Cancel', IconNames.CROSS, this.handleCloseDialog)} +
+
+
+
+ ); + } + + private dateInput = () => { + return ( + + ); + }; + + private parseDate = (str: string) => new Date(str); + + private formatDate = (date: Date) => date.toLocaleString(); + + private handleDateChange = (selectedDate: Date) => { + if (this.props.forOpenDate) { + this.setState({ openAt: selectedDate }); + } else { + this.setState({ closeAt: selectedDate }); + } + }; + + private handleCloseDialog = () => this.setState({ dialogOpen: false }); + private handleOpenDialog = () => this.setState({ dialogOpen: true }); + private handleUpdate = () => { + const { data } = this.props; + this.props.handleAssessmentChangeDate( + data.id, + this.state.openAt.toISOString(), + this.state.closeAt.toISOString() + ); + this.handleCloseDialog(); + }; +} + +export default EditCell; diff --git a/src/components/groundControl/GroundControl.tsx b/src/components/groundControl/GroundControl.tsx new file mode 100644 index 0000000000..a054424580 --- /dev/null +++ b/src/components/groundControl/GroundControl.tsx @@ -0,0 +1,230 @@ +import { ColDef, GridApi, GridReadyEvent } from 'ag-grid'; +import { AgGridReact } from 'ag-grid-react'; +import 'ag-grid/dist/styles/ag-grid.css'; +import 'ag-grid/dist/styles/ag-theme-balham.css'; +import { sortBy } from 'lodash'; +import * as React from 'react'; + +import { getPrettyDate } from '../../utils/dateHelpers'; +import { IAssessmentOverview } from '../assessment/assessmentShape'; +import ContentDisplay from '../commons/ContentDisplay'; +import DeleteCell from './DeleteCell'; +import Dropzone from './Dropzone'; +import EditCell from './EditCell'; +import PublishCell from './PublishCell'; + +export interface IDispatchProps { + handleAssessmentOverviewFetch: () => void; + handleDeleteAssessment: (id: number) => void; + handleUploadAssessment: (file: File, forceUpdate: boolean) => void; + handlePublishAssessment: (togglePublishTo: boolean, id: number) => void; + handleAssessmentChangeDate: (id: number, openAt: string, closeAt: string) => void; +} + +export interface IGroundControlAssessmentOverview extends IAssessmentOverview { + prettyOpenAt: string; + prettyCloseAt: string; + formattedOpenAt: Date; + formattedCloseAt: Date; +} + +export interface IStateProps { + assessmentOverviews: IAssessmentOverview[]; +} + +export interface IGroundControlProps extends IDispatchProps, IStateProps {} + +interface IGroundControlState { + forceUpdate: boolean; + displayConfirmation: boolean; +} + +class GroundControl extends React.Component { + private columnDefs: ColDef[]; + private gridApi?: GridApi; + + public constructor(props: IGroundControlProps) { + super(props); + this.state = { + forceUpdate: false, + displayConfirmation: false + }; + this.columnDefs = [ + { + headerName: 'Title', + field: 'title' + }, + { + headerName: 'Category', + field: 'category', + width: 100 + }, + { + headerName: 'Open Date', + field: '', + cellRendererFramework: EditCell, + cellRendererParams: { + handleAssessmentChangeDate: this.props.handleAssessmentChangeDate, + forOpenDate: true + }, + width: 150, + suppressSorting: true, + suppressMovable: true, + suppressMenu: true, + cellStyle: { + padding: 0 + } + }, + { + headerName: 'Close Date', + field: '', + cellRendererFramework: EditCell, + cellRendererParams: { + handleAssessmentChangeDate: this.props.handleAssessmentChangeDate, + forOpenDate: false + }, + width: 150, + suppressSorting: true, + suppressMovable: true, + suppressMenu: true, + cellStyle: { + padding: 0 + } + }, + { + headerName: 'Publish', + field: '', + cellRendererFramework: PublishCell, + cellRendererParams: { + handlePublishAssessment: this.props.handlePublishAssessment + }, + width: 100, + suppressSorting: true, + suppressMovable: true, + suppressMenu: true, + cellStyle: { + padding: 0 + }, + hide: !this.props.handlePublishAssessment + }, + { + headerName: 'Delete', + field: '', + cellRendererFramework: DeleteCell, + cellRendererParams: { + handleDeleteAssessment: this.props.handleDeleteAssessment + }, + width: 100, + suppressSorting: true, + suppressMovable: true, + suppressMenu: true, + cellStyle: { + padding: 0 + }, + hide: !this.props.handleDeleteAssessment + } + ]; + } + + public componentDidUpdate(prevProps: IGroundControlProps) { + if ( + this.gridApi && + this.props.assessmentOverviews.length !== prevProps.assessmentOverviews.length + ) { + this.gridApi.setRowData(this.sortByCategoryAndDate()); + } + } + + public render() { + const data = this.sortByCategoryAndDate(); + const Grid = () => ( +
+
+ +
+
+ ); + + const display = ( +
+ + +
+ ); + + return ( +
+ +
+ ); + } + + private sortByCategoryAndDate = () => { + if (!this.props.assessmentOverviews) { + return []; + } + + const overview: IGroundControlAssessmentOverview[] = this.props.assessmentOverviews + .slice() + .map(assessmentOverview => { + const clone: IGroundControlAssessmentOverview = JSON.parse( + JSON.stringify(assessmentOverview) + ); + clone.prettyCloseAt = getPrettyDate(clone.closeAt); + clone.prettyOpenAt = getPrettyDate(clone.openAt); + clone.formattedOpenAt = new Date(Date.parse(clone.openAt)); + clone.formattedCloseAt = new Date(Date.parse(clone.closeAt)); + return clone; + }); + return sortBy(overview, ['category', 'formattedOpenAt', 'formattedCloseAt']); + }; + + private onGridReady = (params: GridReadyEvent) => { + this.gridApi = params.api; + this.gridApi.sizeColumnsToFit(); + }; + + private resizeGrid = () => { + if (this.gridApi) { + this.gridApi.sizeColumnsToFit(); + } + }; + + private handleUploadAssessment = (file: File) => { + this.props.handleUploadAssessment(file, this.state.forceUpdate); + this.setState({ forceUpdate: false }); + }; + + private toggleForceUpdate = () => { + this.setState({ forceUpdate: !this.state.forceUpdate }); + }; + + private toggleDisplayConfirmation = () => { + this.setState({ displayConfirmation: !this.state.displayConfirmation }); + }; +} + +export default GroundControl; diff --git a/src/components/groundControl/PublishCell.tsx b/src/components/groundControl/PublishCell.tsx new file mode 100644 index 0000000000..0874c81688 --- /dev/null +++ b/src/components/groundControl/PublishCell.tsx @@ -0,0 +1,70 @@ +import { Classes, Dialog, Switch } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; + +import { IAssessmentOverview } from '../assessment/assessmentShape'; +import { controlButton } from '../commons'; + +interface IPublishCellProps { + data: IAssessmentOverview; + handlePublishAssessment: (togglePublishTo: boolean, id: number) => void; +} + +interface IPublishCellState { + dialogOpen: boolean; + isPublished: boolean; +} + +class PublishCell extends React.Component { + public constructor(props: IPublishCellProps) { + super(props); + this.state = { + dialogOpen: false, + isPublished: this.props.data.isPublished === undefined ? false : this.props.data.isPublished + }; + } + + public render() { + const text = this.props.data.isPublished ? 'Unpublish' : 'Publish'; + const lowerCaseText = text.toLowerCase(); + const toggleButton = () => { + return ( +
+ +
+ ); + }; + return ( +
+ {toggleButton()} + +
+ {

Are you sure that you want to {lowerCaseText} this Assessment?

} +
+
+
+ {controlButton('Confirm ' + text, IconNames.CONFIRM, this.handleDelete)} + {controlButton('Cancel', IconNames.CROSS, this.handleCloseDialog)} +
+
+
+
+ ); + } + + private handleCloseDialog = () => this.setState({ dialogOpen: false }); + private handleOpenDialog = () => this.setState({ dialogOpen: true }); + private handleDelete = () => { + const { data } = this.props; + this.props.handlePublishAssessment(!data.isPublished, data.id); + this.handleCloseDialog(); + }; +} + +export default PublishCell; diff --git a/src/components/workspace/Editor.tsx b/src/components/workspace/Editor.tsx old mode 100755 new mode 100644 index 6f4585f440..ec6a5a0fe8 --- a/src/components/workspace/Editor.tsx +++ b/src/components/workspace/Editor.tsx @@ -45,6 +45,8 @@ export interface IEditorProps { handleDeclarationNavigate: (cursorPosition: IPosition) => void; handleEditorEval: () => void; handleEditorValueChange: (newCode: string) => void; + handleReplValueChange?: (newCode: string) => void; + handleReplEval?: () => void; handleEditorUpdateBreakpoints: (breakpoints: string[]) => void; handleFinishInvite?: () => void; handlePromptAutocomplete: (row: number, col: number, callback: any) => void; diff --git a/src/containers/PlaygroundContainer.ts b/src/containers/PlaygroundContainer.ts old mode 100755 new mode 100644 diff --git a/src/containers/groundControl/GroundControlContainer.ts b/src/containers/groundControl/GroundControlContainer.ts new file mode 100644 index 0000000000..84610c527d --- /dev/null +++ b/src/containers/groundControl/GroundControlContainer.ts @@ -0,0 +1,36 @@ +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { + changeDateAssessment, + deleteAssessment, + publishAssessment, + uploadAssessment +} from '../../actions/groundControl'; +import { fetchAssessmentOverviews } from '../../actions/session'; +import GroundControl, { + IDispatchProps, + IStateProps +} from '../../components/groundControl/GroundControl'; +import { IState } from '../../reducers/states'; + +const mapStateToProps: MapStateToProps = state => ({ + assessmentOverviews: state.session.assessmentOverviews ? state.session.assessmentOverviews : [] +}); + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + handleAssessmentChangeDate: changeDateAssessment, + handleAssessmentOverviewFetch: fetchAssessmentOverviews, + handleDeleteAssessment: deleteAssessment, + handleUploadAssessment: uploadAssessment, + handlePublishAssessment: publishAssessment + }, + dispatch + ); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(GroundControl); diff --git a/src/reducers/states.ts b/src/reducers/states.ts old mode 100755 new mode 100644 diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index 5240c8d4cd..2d3bbbac5a 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -556,7 +556,95 @@ function* backendSaga(): SagaIterator { yield put(actions.fetchMaterialIndex(parentId)); yield call(showSuccessMessage, 'Deleted successfully!', 1000); }); + yield takeEvery(actionTypes.CHANGE_DATE_ASSESSMENT, function*( + action: ReturnType + ) { + const tokens = yield select((state: IState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + const id = action.payload.id; + const closeAt = action.payload.closeAt; + const openAt = action.payload.openAt; + const respMsg: string | null = yield request.changeDateAssessment(id, closeAt, openAt, tokens); + if (respMsg == null) { + yield request.handleResponseError(respMsg); + return; + } else if (respMsg !== 'OK') { + yield call(showWarningMessage, respMsg, 5000); + return; + } + + yield put(actions.fetchAssessmentOverviews()); + yield call(showSuccessMessage, 'Updated successfully!', 1000); + }); + + yield takeEvery(actionTypes.DELETE_ASSESSMENT, function*( + action: ReturnType + ) { + const tokens = yield select((state: IState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + const id = action.payload; + const resp: Response = yield request.deleteAssessment(id, tokens); + + if (!resp || !resp.ok) { + yield request.handleResponseError(resp); + return; + } + + yield put(actions.fetchAssessmentOverviews()); + yield call(showSuccessMessage, 'Deleted successfully!', 1000); + }); + yield takeEvery(actionTypes.PUBLISH_ASSESSMENT, function*( + action: ReturnType + ) { + const tokens = yield select((state: IState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + const id = action.payload.id; + const togglePublishTo = action.payload.togglePublishTo; + const resp: Response = yield request.publishAssessment(id, togglePublishTo, tokens); + + if (!resp || !resp.ok) { + yield request.handleResponseError(resp); + return; + } + + yield put(actions.fetchAssessmentOverviews()); + + if (togglePublishTo) { + yield call(showSuccessMessage, 'Published successfully!', 1000); + } else { + yield call(showSuccessMessage, 'Unpublished successfully!', 1000); + } + }); + + yield takeEvery(actionTypes.UPLOAD_ASSESSMENT, function*( + action: ReturnType + ) { + const tokens = yield select((state: IState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + const file = action.payload.file; + const forceUpdate = action.payload.forceUpdate; + const respMsg = yield request.uploadAssessment(file, tokens, forceUpdate); + if (!respMsg) { + yield request.handleResponseError(respMsg); + } else if (respMsg === 'OK') { + yield call(showSuccessMessage, 'Uploaded successfully!', 2000); + } else if (respMsg === 'Force Update OK') { + yield call(showSuccessMessage, 'Assessment force updated successfully!', 2000); + } else { + yield call(showWarningMessage, respMsg, 10000); + return; + } + yield put(actions.fetchAssessmentOverviews()); + }); yield takeEvery(actionTypes.FETCH_TEST_STORIES, function*( action: ReturnType ) { @@ -580,7 +668,6 @@ function* backendSaga(): SagaIterator { return; } } - resp = yield call(request.getMaterialIndex, -1, tokens); if (resp) { materialIndex = resp.index; diff --git a/src/sagas/requests.ts b/src/sagas/requests.ts index cf047d9e5f..82c0f069b3 100644 --- a/src/sagas/requests.ts +++ b/src/sagas/requests.ts @@ -611,6 +611,62 @@ export const postMaterialFolder = async (title: string, parentId: number, tokens return resp; }; +export async function changeDateAssessment( + id: number, + closeAt: string, + openAt: string, + tokens: Tokens +) { + const resp = await request(`assessments/update/${id}`, 'POST', { + accessToken: tokens.accessToken, + body: { closeAt, openAt }, + noHeaderAccept: true, + refreshToken: tokens.refreshToken, + shouldAutoLogout: false, + shouldRefresh: true + }); + return resp ? await resp.text() : null; +} + +export async function deleteAssessment(id: number, tokens: Tokens) { + const resp = await request(`assessments/${id}`, 'DELETE', { + accessToken: tokens.accessToken, + noHeaderAccept: true, + refreshToken: tokens.refreshToken, + shouldAutoLogout: false, + shouldRefresh: true + }); + return resp; +} + +export async function publishAssessment(id: number, togglePublishTo: boolean, tokens: Tokens) { + const resp = await request(`assessments/publish/${id}`, 'POST', { + accessToken: tokens.accessToken, + body: { togglePublishTo }, + noHeaderAccept: true, + refreshToken: tokens.refreshToken, + shouldAutoLogout: false, + shouldRefresh: true + }); + return resp; +} + +export const uploadAssessment = async (file: File, tokens: Tokens, forceUpdate: boolean) => { + const formData = new FormData(); + formData.append('assessment[file]', file); + formData.append('forceUpdate', String(forceUpdate)); + const resp = await request(`assessments`, 'POST', { + accessToken: tokens.accessToken, + body: formData, + noContentType: true, + noHeaderAccept: true, + refreshToken: tokens.refreshToken, + shouldAutoLogout: false, + shouldRefresh: true + }); + return resp ? await resp.text() : null; +}; + /** * @returns {(Response|null)} Response if successful, otherwise null. * diff --git a/src/styles/_groundcontrol.scss b/src/styles/_groundcontrol.scss new file mode 100644 index 0000000000..057f083749 --- /dev/null +++ b/src/styles/_groundcontrol.scss @@ -0,0 +1,7 @@ +.GroundControl { + .toggle-button-wrapper { + margin-left: auto; + margin-right: auto; + width: 20px; + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss index b276917cf3..b255120a54 100755 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,5 +1,6 @@ @import '~normalize.css/normalize.css'; @import '~@blueprintjs/core/lib/css/blueprint.css'; +@import '~@blueprintjs/datetime/lib/css/blueprint-datetime.css'; @import '~flexboxgrid/dist/flexboxgrid.css'; @import '~flexboxgrid-helpers/dist/flexboxgrid-helpers.min.css'; @@ -23,6 +24,7 @@ @import 'contributors'; @import 'dropdown'; @import 'game'; +@import 'groundcontrol'; @import 'login'; @import 'material'; @import 'navigationBar'; diff --git a/src/utils/constants.ts b/src/utils/constants.ts old mode 100755 new mode 100644 From a46a333b16a03d3599fa0c17501d9e676b37e8a5 Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 00:58:42 +0800 Subject: [PATCH 28/39] Using SessionStorage for Game State instead of JS --- .../academy/game/backend/game-state.js | 104 +++++++++++------- .../academy/game/save-manager/save-manager.js | 4 + src/components/game-dev/JsonUpload.tsx | 18 ++- 3 files changed, 79 insertions(+), 47 deletions(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 14f766bec6..e67c270e89 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -10,79 +10,95 @@ var SaveManager = require('../save-manager/save-manager.js'); * - The global list of missions that are open * - The action to save user state to server. */ -let fetched = false; -let studentData = undefined, - handleSaveData = undefined, - studentStory = undefined; +let handleSaveData = undefined; -export function fetchGameData(userStory, gameState, callback) { +//everything is going to be stored in session storage since we discovered game-state dosent persist over pages +const OVERRIDE_KEY = "key for SA2021 overridden", +SESSION_DATA_KEY = "key for SA2021 sessionData"; + +export function fetchGameData(story, gameStates, callback) { + console.log("fetch game data"); + console.log(story); + console.log(gameStates); + console.log(callback); // fetch only needs to be called once; if there are additional calls somehow then ignore them - if(fetched) { - callback(); - return; + // if(hasBeenFetched()) { + // callback(); + // return; + // } + console.log("storing into session storage"); + let data = { + "story":story, "gameStates":gameStates, "currentDate":Date() } - fetched = true; - studentStory = userStory.story; - studentData = gameState; + sessionStorage.setItem(SESSION_DATA_KEY, JSON.stringify(data)); + console.log(sessionStorage.getItem(SESSION_DATA_KEY)); fetchGlobalMissionPointer(callback); } -// overrides -let studentDataOverride = undefined, - currentDateOverride = undefined, - studentStoryOverride = undefined; +function printSessionData() { + console.log("SessionData = " + (sessionStorage.getItem(SESSION_DATA_KEY))); +} + +function setSessionData(data) { + sessionStorage.setItem(SESSION_DATA_KEY, JSON.stringify(data)); +} + +function getSessionData() { + return JSON.parse(sessionStorage.getItem(SESSION_DATA_KEY)); +} + +function hasBeenFetched() { + return sessionStorage.hasOwnProperty(SESSION_DATA_KEY); +} -// override student game data -export function overrideGameState(data) { +// override student session data +export function overrideSessionData(data) { + setSessionData(data); if (data) { - studentDataOverride = data.gameState; - studentStoryOverride = data.story; - currentDateOverride = data.currentDate; + sessionStorage.setItem(OVERRIDE_KEY, "true"); } else { - studentStoryOverride = studentDataOverride = currentDateOverride = undefined; + sessionStorage.removeItem(OVERRIDE_KEY); } + printSessionData(); } export function setSaveHandler(saveData) { handleSaveData = saveData; } -export function getStudentData() { - // formerly create-initializer/loadFromServer - if(studentDataOverride) return studentDataOverride; - return studentData; -} - -export function saveStudentData(data) { +export function saveUserData(data) { console.log('saving student data'); - if (handleSaveData !== undefined) { + if (data && handleSaveData !== undefined) { handleSaveData(data) + setSessionData(data); } } export function saveCollectible(collectible) { - studentData.collectibles[collectible] = 'completed'; - saveStudentData(studentData); + const data = getSessionData(); + data.gameStates.collectibles[collectible] = 'completed'; + saveUserData(data); } export function hasCollectible(collectible) { - return studentData && - studentData.collectibles[collectible] && - studentData.collectibles[collectible] === 'completed'; + return hasBeenFetched() && + getSessionData().gameStates.collectibles[collectible] === 'completed'; } export function saveQuest(questId) { - studentData.completed_quests.push(questId); - saveStudentData(studentData); + const data = getSessionData(); + data.gameStates.completed_quests.push(questId); + saveUserData(data); } export function hasCompletedQuest(questId) { - return studentData && studentData.completed_quests.includes(questId); + return hasBeenFetched() && + getSessionData().gameStates.completed_quests.includes(questId); } function getStudentStory() { - if(studentStoryOverride) return studentStoryOverride; - return studentStory; + return hasBeenFetched() && + getSessionData().story.story; } let stories = []; @@ -93,11 +109,13 @@ function fetchGlobalMissionPointer(callback) { url: (isTest ? storyXMLPathTest : storyXMLPathLive) + 'master.xml', dataType: 'xml', success: xml => { + console.log(xml.children[0].children); stories = Array.from(xml.children[0].children); stories = stories.sort((a, b) => parseInt(a.getAttribute("key")) - parseInt(b.getAttribute("key"))); - const now = currentDateOverride ? currentDateOverride : new Date(); + const now = new Date(getSessionData().currentDate); const openStory = story => new Date(story.getAttribute("startDate")) < now && now < new Date(story.getAttribute("endDate")); stories = stories.filter(openStory); + console.log(stories); callback(); }, error: isTest @@ -120,12 +138,14 @@ function fetchGlobalMissionPointer(callback) { */ export function getMissionPointer() { //finds the mission id's mission pointer - let missionPointer = stories.find(story => story.getAttribute("id") === getStudentStory()); + const studentStory = getStudentStory(); + console.log("students story " + studentStory); + let missionPointer = stories.find(story => !studentStory || story.getAttribute("id") === studentStory).getKey(); const newest = parseInt(stories[stories.length-1].getAttribute("key")); // the newest mission to open const oldest = parseInt(stories[0].getAttribute("key")); // the oldest mission to open missionPointer = Math.min(missionPointer, newest); missionPointer = Math.max(missionPointer, oldest); - const storyToLoad = stories.filter(story => story.getAttribute("key") == missionPointer)[0]; + const storyToLoad = stories.filter(story => story.getAttribute("id") == missionPointer)[0]; console.log("Now loading story " + storyToLoad.getAttribute("id")); // debug statement return storyToLoad.getAttribute("id"); } \ No newline at end of file diff --git a/src/components/academy/game/save-manager/save-manager.js b/src/components/academy/game/save-manager/save-manager.js index d741c60dc5..75c4d2a65b 100644 --- a/src/components/academy/game/save-manager/save-manager.js +++ b/src/components/academy/game/save-manager/save-manager.js @@ -24,6 +24,10 @@ export function init() { } } + //callback wasn't being used, but was required in location manager + // Made it an empty function + let callback = () => {}; + StoryManager.loadStoryXML(storyXMLs, false, function() { LocationManager.changeStartLocation(saveData.startLocation); if (hasPending()) { diff --git a/src/components/game-dev/JsonUpload.tsx b/src/components/game-dev/JsonUpload.tsx index 7067880ed8..f1b786db17 100644 --- a/src/components/game-dev/JsonUpload.tsx +++ b/src/components/game-dev/JsonUpload.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { overrideGameState } from '../academy/game/backend/game-state.js'; +import { overrideSessionData } from '../academy/game/backend/game-state.js'; class JsonUpload extends React.Component { private static onFormSubmit(e: { preventDefault: () => void }) { @@ -8,7 +8,7 @@ class JsonUpload extends React.Component { constructor(props: Readonly<{}>) { super(props); - overrideGameState(undefined); + overrideSessionData(undefined); JsonUpload.onFormSubmit = JsonUpload.onFormSubmit.bind(this); this.onChange = this.onChange.bind(this); } @@ -23,10 +23,18 @@ class JsonUpload extends React.Component { } private onChange(e: { target: { files: any } }) { const reader = new FileReader(); - reader.onload = (event: Event) => { - overrideGameState(JSON.parse('' + reader.result)); + reader.onloadend = (event: Event) => { + if(typeof reader.result === "string") { + overrideSessionData(JSON.parse(reader.result)); + } }; - reader.readAsText(e.target.files[0]); + if (e.target.files[0] instanceof Blob) { + reader.readAsText(e.target.files[0]); + } else { + overrideSessionData(undefined); + } + + } } From 6ae6dd2c8f12fd471e70eb92fef7e117948ea3bb Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 05:06:49 +0800 Subject: [PATCH 29/39] Minor Cosmetic Changes --- src/components/academy/game/constants/constants.js | 4 ++-- src/sagas/backend.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/academy/game/constants/constants.js b/src/components/academy/game/constants/constants.js index d4c3b09bf4..96eb589f79 100644 --- a/src/components/academy/game/constants/constants.js +++ b/src/components/academy/game/constants/constants.js @@ -24,7 +24,7 @@ export const avatarPath = ASSETS_HOST + 'avatars/', uiPath = ASSETS_HOST + 'UI/', soundPath = ASSETS_HOST + 'sounds/', - saveDataKey = "source_academy_save_data", - locationKey = "source_academy_location", + SAVE_DATA_KEY = "source_academy_save_data", + LOCATION_KEY = "source_academy_location", fadeTime = 0.3, nullFunction = function() {}; \ No newline at end of file diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index 2d3bbbac5a..e49035f30c 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -645,6 +645,7 @@ function* backendSaga(): SagaIterator { } yield put(actions.fetchAssessmentOverviews()); }); + yield takeEvery(actionTypes.FETCH_TEST_STORIES, function*( action: ReturnType ) { From 57247221e10307bd81ebd71bb7ce9e140396f720 Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 05:07:57 +0800 Subject: [PATCH 30/39] GamePage to load only when all information is gotten --- src/components/academy/game/index.tsx | 42 ++++++++++++++++++++++----- src/containers/GameContainer.ts | 8 +++-- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/components/academy/game/index.tsx b/src/components/academy/game/index.tsx index 8ca617aeb0..3a57444f64 100644 --- a/src/components/academy/game/index.tsx +++ b/src/components/academy/game/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { IAssessmentOverview } from 'src/components/assessment/assessmentShape'; import { GameState, Role, Story } from '../../../reducers/states'; import { setSaveHandler } from './backend/game-state'; import { setUserRole } from './backend/user'; @@ -8,14 +9,16 @@ type GameProps = DispatchProps & StateProps; export type DispatchProps = { handleSaveCanvas: (c: HTMLCanvasElement) => void; handleSaveData: (s: GameState) => void; + handleAssessmentOverviewFetch: () => void; }; export type StateProps = { canvas?: HTMLCanvasElement; - name: string; + name?: string; story: Story; gameState: GameState; role?: Role; + assessmentOverviews?: IAssessmentOverview[]; }; export class Game extends React.Component { @@ -39,20 +42,43 @@ export class Game extends React.Component { * backend sends us 'playStory', which is the negation (!) of `attemptedAll`. */ public async componentDidMount() { - const story: any = (await import('./game.js')).default; - if (this.props.canvas === undefined) { - setUserRole(this.props.role); - setSaveHandler((gameState: GameState) => this.props.handleSaveData(gameState)); - - story(this.div, this.canvas, this.props.name, this.props.story, this.props.gameState); + if (this.props.name && this.props.role && !this.props.assessmentOverviews) { + // If assessment overviews are not loaded, fetch them + this.props.handleAssessmentOverviewFetch(); + const loadingScreen: any = (await import('./story-xml-player.js')).loadingScreen; + loadingScreen(this.div, this.canvas); this.props.handleSaveCanvas(this.canvas); - } else { + } + if (this.props.canvas !== undefined) { // This browser window has loaded the Game component & canvas before + this.canvas = this.props.canvas; this.div.innerHTML = ''; this.div.appendChild(this.props.canvas); } } + public async componentDidUpdate(prevProps: Readonly) { + // loads only once after assessmentOverviews are up + const isLoaded = + this.props.name && this.props.role && this.props.assessmentOverviews && this.props.canvas; + const prevLoaded = + prevProps.name && prevProps.role && prevProps.assessmentOverviews && prevProps.canvas; + if (isLoaded && isLoaded !== prevLoaded) { + const story: any = (await import('./game.js')).default; + setUserRole(this.props.role); + setSaveHandler((gameState: GameState) => this.props.handleSaveData(gameState)); + story( + this.div, + this.canvas, + this.props.name, + this.props.story, + this.props.gameState, + this.props.assessmentOverviews + ); + this.props.handleSaveCanvas(this.canvas); + } + } + public render() { return (
(this.div = e!)}> diff --git a/src/containers/GameContainer.ts b/src/containers/GameContainer.ts index 940dcdf95f..9306a387c5 100644 --- a/src/containers/GameContainer.ts +++ b/src/containers/GameContainer.ts @@ -1,7 +1,7 @@ import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { saveCanvas, saveUserData } from '../actions/game'; +import { fetchAssessmentOverviews, saveCanvas, saveUserData } from '../actions'; import Game, { DispatchProps, StateProps } from '../components/academy/game'; import { IState } from '../reducers/states'; @@ -9,7 +9,8 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis bindActionCreators( { handleSaveCanvas: saveCanvas, - handleSaveData: saveUserData + handleSaveData: saveUserData, + handleAssessmentOverviewFetch: fetchAssessmentOverviews }, dispatch ); @@ -19,7 +20,8 @@ const mapStateToProps: MapStateToProps = state => ({ name: state.session.name!, story: state.session.story, gameState: state.session.gameState, - role: state.session.role + role: state.session.role, + assessmentOverviews: state.session.assessmentOverviews }); export default connect( From 18b96fdd229a88587285f9e67d9ed218a07435ea Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 05:08:24 +0800 Subject: [PATCH 31/39] Fixed some errors and allow for reset json files --- src/components/game-dev/JsonUpload.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/game-dev/JsonUpload.tsx b/src/components/game-dev/JsonUpload.tsx index f1b786db17..b15a19b704 100644 --- a/src/components/game-dev/JsonUpload.tsx +++ b/src/components/game-dev/JsonUpload.tsx @@ -15,26 +15,25 @@ class JsonUpload extends React.Component { public render() { return ( -
+

Game State Override

); } - private onChange(e: { target: { files: any } }) { + private onChange(e: { target: any }) { const reader = new FileReader(); reader.onloadend = (event: Event) => { - if(typeof reader.result === "string") { + if (typeof reader.result === 'string') { overrideSessionData(JSON.parse(reader.result)); } }; - if (e.target.files[0] instanceof Blob) { + if (e.target.files && e.target.files[0] instanceof Blob) { reader.readAsText(e.target.files[0]); } else { overrideSessionData(undefined); + e.target.value = null; } - - } } From b049f64f57ff9686745a257ae73de42b1f231124 Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 05:13:04 +0800 Subject: [PATCH 32/39] Updated Game Components to use GroundControl Use Ground Control's assessment overview instead of master.xml file. Less things to keep track of this way. --- .../academy/game/backend/game-state.js | 191 +++++++++++------- .../academy/game/create-initializer.js | 13 +- src/components/academy/game/game.js | 4 +- .../academy/game/save-manager/save-manager.js | 22 +- .../game/story-manager/story-manager.js | 1 - .../academy/game/story-xml-player.js | 23 ++- 6 files changed, 169 insertions(+), 85 deletions(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index e67c270e89..92b1cd30d0 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -1,4 +1,4 @@ -import { storyXMLPathTest, storyXMLPathLive } from '../constants/constants' +import { storyXMLPathTest, storyXMLPathLive, SAVE_DATA_KEY, LOCATION_KEY } from '../constants/constants' import { isStudent } from './user'; var SaveManager = require('../save-manager/save-manager.js'); @@ -13,34 +13,40 @@ var SaveManager = require('../save-manager/save-manager.js'); let handleSaveData = undefined; //everything is going to be stored in session storage since we discovered game-state dosent persist over pages -const OVERRIDE_KEY = "key for SA2021 overridden", -SESSION_DATA_KEY = "key for SA2021 sessionData"; - -export function fetchGameData(story, gameStates, callback) { - console.log("fetch game data"); - console.log(story); - console.log(gameStates); - console.log(callback); +const OVERRIDE_KEY = "source_academy_override", +OVERRIDE_DATES_KEY = "source_academy_override_dates", +OVERRIDE_PUBLISH_KEY = "source_academy_override_publish", +SESSION_DATA_KEY = "source_academy_session_data"; + +let sessionData = undefined; + +export function fetchGameData(story, gameStates, missions, callback) { // fetch only needs to be called once; if there are additional calls somehow then ignore them - // if(hasBeenFetched()) { - // callback(); - // return; - // } - console.log("storing into session storage"); - let data = { - "story":story, "gameStates":gameStates, "currentDate":Date() + // only for students + if(!hasBeenFetched() && isStudent()) { + let data = { + "story":story, "gameStates":gameStates, "currentDate":Date() + } + setSessionData(data); + } else if (isStudent()) { + callback(getSessionData().story.story); + return; + } + if (!isStudent()) { + // resets current progress for testers + SaveManager.resetLocalSaveData(); } - sessionStorage.setItem(SESSION_DATA_KEY, JSON.stringify(data)); - console.log(sessionStorage.getItem(SESSION_DATA_KEY)); - fetchGlobalMissionPointer(callback); + printSessionData(); + missions = organiseMissions(missions); + getMissionPointer(missions, callback); } function printSessionData() { console.log("SessionData = " + (sessionStorage.getItem(SESSION_DATA_KEY))); } -function setSessionData(data) { - sessionStorage.setItem(SESSION_DATA_KEY, JSON.stringify(data)); +function setSessionData(sessionData) { + sessionStorage.setItem(SESSION_DATA_KEY, JSON.stringify(sessionData)); } function getSessionData() { @@ -51,13 +57,28 @@ function hasBeenFetched() { return sessionStorage.hasOwnProperty(SESSION_DATA_KEY); } +function removeSessionStorage() { + sessionStorage.removeItem(SESSION_DATA_KEY); + sessionStorage.removeItem(OVERRIDE_KEY); + sessionStorage.removeItem(OVERRIDE_PUBLISH_KEY); + sessionStorage.removeItem(OVERRIDE_DATES_KEY); +} + + + // override student session data export function overrideSessionData(data) { - setSessionData(data); if (data) { + setSessionData(data.sessionData); sessionStorage.setItem(OVERRIDE_KEY, "true"); + if (data.overridePublish) { + sessionStorage.setItem(OVERRIDE_PUBLISH_KEY, "will override published"); + } + if (data.overrideDates) { + sessionStorage.setItem(OVERRIDE_DATES_KEY, "will override dates"); + } } else { - sessionStorage.removeItem(OVERRIDE_KEY); + removeSessionStorage(); } printSessionData(); } @@ -67,7 +88,6 @@ export function setSaveHandler(saveData) { } export function saveUserData(data) { - console.log('saving student data'); if (data && handleSaveData !== undefined) { handleSaveData(data) setSessionData(data); @@ -75,9 +95,9 @@ export function saveUserData(data) { } export function saveCollectible(collectible) { - const data = getSessionData(); + const sessionData = getSessionData(); data.gameStates.collectibles[collectible] = 'completed'; - saveUserData(data); + saveUserData(sessionData); } export function hasCollectible(collectible) { @@ -86,9 +106,9 @@ export function hasCollectible(collectible) { } export function saveQuest(questId) { - const data = getSessionData(); - data.gameStates.completed_quests.push(questId); - saveUserData(data); + const sessionData = getSessionData(); + sessionData.gameStates.completed_quests.push(questId); + saveUserData(sessionData); } export function hasCompletedQuest(questId) { @@ -96,39 +116,62 @@ export function hasCompletedQuest(questId) { getSessionData().gameStates.completed_quests.includes(questId); } + function getStudentStory() { - return hasBeenFetched() && - getSessionData().story.story; + //tries to retrieve local version of story + //if unable to find, use backend's version. + if (SaveManager.hasLocalSave()) { + let actionSequence = SaveManager.getLocalSaveData().actionSequence; + let story = actionSequence[actionSequence.length - 1].storyID; + return story; + } else { + return hasBeenFetched() + ? getSessionData().story.story + : undefined; + } } -let stories = []; - -function fetchGlobalMissionPointer(callback) { - const makeAjax = isTest => $.ajax({ - type: 'GET', - url: (isTest ? storyXMLPathTest : storyXMLPathLive) + 'master.xml', - dataType: 'xml', - success: xml => { - console.log(xml.children[0].children); - stories = Array.from(xml.children[0].children); - stories = stories.sort((a, b) => parseInt(a.getAttribute("key")) - parseInt(b.getAttribute("key"))); - const now = new Date(getSessionData().currentDate); - const openStory = story => new Date(story.getAttribute("startDate")) < now && now < new Date(story.getAttribute("endDate")); - stories = stories.filter(openStory); - console.log(stories); - callback(); - }, - error: isTest - ? () => { - console.log('Cannot find master story list on test'); - console.log('Trying on live...'); - makeAjax(false); - } - : () => { - console.error('Cannot find master story list'); - } - }); - makeAjax(!isStudent()); +function organiseMissions(missions) { + function compareMissions(x, y) { + //compares with opening dates first and if equal, compare closing dates. + //sort by earliest date + const openX = new Date(x.openAt).getTime(); + const openY = new Date(y.openAt).getTime(); + const closeX = new Date(x.closeAt).getTime(); + const closeY = new Date(y.closeAt).getTime(); + return openX === openY + ? Math.sign(closeX - closeY) + : Math.sign(openX - openY); + } + function isWithinDates(mission, date) { + return new Date(mission.openAt) <= now && + now <= new Date(mission.closeAt); + } + let predicate; + const now = new Date(getSessionData().currentDate); + + if (isStudent()) { + predicate = (mission) => + mission.isPublished && isWithinDates(mission, now); + } else { + // resets current progress + SaveManager.resetLocalSaveData(); + //testers will play unpublished missions too and can go out of bounds unless + //they state that they don't want to in the json file, using the override keys + predicate = (mission) => { + let toPass = true; + if (!sessionStorage.getItem(OVERRIDE_DATES_KEY)) { + toPass = isWithinDates(mission, now); + } + if (!sessionStorage.getItem(OVERRIDE_PUBLISH_KEY)) { + toPass = toPass && mission.isPublished; + } + return toPass; + } + } + missions = missions.filter(predicate); + missions = missions.sort(compareMissions); + return missions; } /** @@ -136,16 +179,24 @@ function fetchGlobalMissionPointer(callback) { * However, in the event the student's current mission pointer falls outside the bounds of the * global list of open missions, then the corresponding upper (or lower) bound will be used. */ -export function getMissionPointer() { +function getMissionPointer(missions, callback) { //finds the mission id's mission pointer - const studentStory = getStudentStory(); - console.log("students story " + studentStory); - let missionPointer = stories.find(story => !studentStory || story.getAttribute("id") === studentStory).getKey(); - const newest = parseInt(stories[stories.length-1].getAttribute("key")); // the newest mission to open - const oldest = parseInt(stories[0].getAttribute("key")); // the oldest mission to open - missionPointer = Math.min(missionPointer, newest); - missionPointer = Math.max(missionPointer, oldest); - const storyToLoad = stories.filter(story => story.getAttribute("id") == missionPointer)[0]; - console.log("Now loading story " + storyToLoad.getAttribute("id")); // debug statement - return storyToLoad.getAttribute("id"); + let studentStory = getStudentStory(); + const isStoryEmpty = story => story === undefined || story.length === 0; + let missionPointer = isStoryEmpty(studentStory) + ? missions[0] + : missions.find(mission => mission.story === studentStory); + //if mission pointer is in localStorage and can't find any proper story. + if (missionPointer === undefined && SaveManager.hasLocalSave()) { + localStorage.removeItem(SAVE_DATA_KEY); + studentStory = getStudentStory(); + missionPointer = isStoryEmpty(studentStory) + ? missions[0] + : missions.find(mission => mission.story === studentStory); + } + if (missionPointer === undefined) { + missionPointer = missions[0]; + } + console.log("Now loading story " + missionPointer.story); // debug statement + callback(missionPointer.story); } \ No newline at end of file diff --git a/src/components/academy/game/create-initializer.js b/src/components/academy/game/create-initializer.js index 2071d82730..2d798b2571 100644 --- a/src/components/academy/game/create-initializer.js +++ b/src/components/academy/game/create-initializer.js @@ -1,9 +1,9 @@ import {LINKS} from '../../../utils/constants' import {history} from '../../../utils/history' -import {soundPath} from './constants/constants' +import {soundPath, LOCATION_KEY} from './constants/constants' import {fetchGameData, getMissionPointer, getStudentData, saveCollectible, saveQuest} from './backend/game-state' -export default function (StoryXMLPlayer, username, userStory, gameState) { +export default function (StoryXMLPlayer, username, userStory, gameState, missions) { var hookHandlers = { startMission: function () { @@ -59,17 +59,16 @@ export default function (StoryXMLPlayer, username, userStory, gameState) { changeLocationHook: function (newLocation) { if (typeof Storage !== 'undefined') { // Code for localStorage/sessionStorage. - localStorage.locationKey = newLocation; + localStorage.setItem(LOCATION_KEY, newLocation); } } }); } - function initialize(div, canvas) { - + function initialize(story, div, canvas) { startGame(div, canvas); - StoryXMLPlayer.loadStory(getMissionPointer(), function () {}); + StoryXMLPlayer.loadStory(story, function () {}); } - return (div, canvas) => fetchGameData(userStory, gameState, () => initialize(div, canvas)); + return (div, canvas) => fetchGameData(userStory, gameState, missions, (story) => initialize(story, div, canvas)); }; diff --git a/src/components/academy/game/game.js b/src/components/academy/game/game.js index 9e47693710..ca2b6436f5 100644 --- a/src/components/academy/game/game.js +++ b/src/components/academy/game/game.js @@ -1,8 +1,8 @@ import createInitializer from './create-initializer' -export default function(div, canvas, username, userStory, gameState) { +export default function(div, canvas, username, userStory, gameState, missions) { var StoryXMLPlayer = require('./story-xml-player'); var container = document.getElementById('game-display') - var initialize = createInitializer(StoryXMLPlayer, username, userStory, gameState) + var initialize = createInitializer(StoryXMLPlayer, username, userStory, gameState, missions) initialize(div, canvas); } diff --git a/src/components/academy/game/save-manager/save-manager.js b/src/components/academy/game/save-manager/save-manager.js index 75c4d2a65b..d771283953 100644 --- a/src/components/academy/game/save-manager/save-manager.js +++ b/src/components/academy/game/save-manager/save-manager.js @@ -1,5 +1,5 @@ import {saveStudentData} from '../backend/game-state'; -import {saveDataKey} from "../constants/constants"; +import {SAVE_DATA_KEY, LOCATION_KEY} from "../constants/constants"; var LocationManager = require('../location-manager/location-manager.js'); var QuestManager = require('../quest-manager/quest-manager.js'); @@ -13,9 +13,8 @@ var actionSequence = []; // finds existing save data, which consists of action sequence and starting location export function init() { - let saveData = localStorage.getItem(saveDataKey); + let saveData = getLocalSaveData(); if (saveData) { - saveData = JSON.parse(saveData); actionSequence = saveData.actionSequence; var storyXMLs = []; for (var i = 0; i < actionSequence.length; i++) { @@ -114,9 +113,24 @@ export function saveLoadStories(stories) { saveGame(); } +export function hasLocalSave() { + return localStorage.hasOwnProperty(SAVE_DATA_KEY); +} + + +export function getLocalSaveData() { + const jsonString = localStorage.getItem(SAVE_DATA_KEY); + return jsonString ? JSON.parse(jsonString) : undefined; +} + +export function resetLocalSaveData() { + localStorage.removeItem(SAVE_DATA_KEY); + localStorage.removeItem(LOCATION_KEY); +} + // saves actionsequence and start location into local storage function saveGame() { - localStorage.setItem(saveDataKey, + localStorage.setItem(SAVE_DATA_KEY, JSON.stringify({ actionSequence: actionSequence, startLocation: LocationManager.getStartLocation() diff --git a/src/components/academy/game/story-manager/story-manager.js b/src/components/academy/game/story-manager/story-manager.js index 1d09839a79..5c30738a56 100644 --- a/src/components/academy/game/story-manager/story-manager.js +++ b/src/components/academy/game/story-manager/story-manager.js @@ -159,7 +159,6 @@ export function loadStoryXML(storyXMLs, willSave, callback) { }, error: isTest ? () => { - console.log('Cannot find story ' + curId + ' on test'); console.log('Trying on live...'); makeAjax(false); } diff --git a/src/components/academy/game/story-xml-player.js b/src/components/academy/game/story-xml-player.js index f02569ce2d..ed92bffb31 100644 --- a/src/components/academy/game/story-xml-player.js +++ b/src/components/academy/game/story-xml-player.js @@ -23,7 +23,6 @@ var stage; export function init(div, canvas, options) { renderer = PIXI.autoDetectRenderer( Constants.screenWidth, - Constants.screenHeight, { backgroundColor: 0x000000, view: canvas } ); @@ -58,6 +57,28 @@ export function init(div, canvas, options) { stage.addChild(ExternalManager.init(options.hookHandlers)); }; +export function loadingScreen(div, canvas) { + renderer = PIXI.autoDetectRenderer( + Constants.screenWidth, + Constants.screenHeight, + { backgroundColor: 0x000000, view: canvas } + ); + div.append(renderer.view); + Utils.saveRenderer(renderer); + // create the root of the scene graph + stage = new PIXI.Container(); + stage.addChild(BlackOverlay.init()); + const loadingScreen = StoryManager.init(); + loadingScreen.visible = true; + stage.addChild(StoryManager.init()); + + function animate() { + requestAnimationFrame(animate); + renderer.render(stage); + } + animate(); +}; + export { getExternalOverlay } from './external-manager/external-manager.js' export { changeStartLocation, gotoStartLocation, gotoLocation } from './location-manager/location-manager.js' export { loadStory, loadStoryWithoutFirstQuest } from './story-manager/story-manager.js' From f1c891eb6383cc64acdced55011daf01208bafd6 Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 05:23:45 +0800 Subject: [PATCH 33/39] Updated Material Table Snapshots --- .../material/__tests__/__snapshots__/MaterialTable.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap b/src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap index b65505b242..cd000a05e5 100644 --- a/src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap +++ b/src/components/material/__tests__/__snapshots__/MaterialTable.tsx.snap @@ -16,7 +16,7 @@ exports[`Material table renders correctly 1`] = `
- +
From 4697453a305ee1602f73341bdd729348e771f16b Mon Sep 17 00:00:00 2001 From: travisryte Date: Sat, 9 May 2020 00:39:37 +0800 Subject: [PATCH 34/39] Minor bugfixes and documentation - Fixed bug with regards to missing stories, testers now use default story if they have not overriden the game. - Potential bug fixed With no missions available, will return leaving a black screen, instead of crashing. - Fixed bug that stops the game from compiling when the last mission has closed, will now only show last opened mission. - Added minor documentation --- .../academy/game/backend/game-state.js | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 92b1cd30d0..9dade53361 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -23,20 +23,22 @@ let sessionData = undefined; export function fetchGameData(story, gameStates, missions, callback) { // fetch only needs to be called once; if there are additional calls somehow then ignore them // only for students - if(!hasBeenFetched() && isStudent()) { - let data = { - "story":story, "gameStates":gameStates, "currentDate":Date() - } - setSessionData(data); - } else if (isStudent()) { + if(hasBeenFetched() && isStudent()) { callback(getSessionData().story.story); return; + } + const data = { + "story":story, + "gameStates":gameStates, + "currentDate": Date() + } + if (!getSessionData()) { + setSessionData(data); } if (!isStudent()) { - // resets current progress for testers + // resets current progress (local storage) for testers SaveManager.resetLocalSaveData(); } - printSessionData(); missions = organiseMissions(missions); getMissionPointer(missions, callback); } @@ -64,8 +66,6 @@ function removeSessionStorage() { sessionStorage.removeItem(OVERRIDE_DATES_KEY); } - - // override student session data export function overrideSessionData(data) { if (data) { @@ -120,6 +120,7 @@ export function hasCompletedQuest(questId) { function getStudentStory() { //tries to retrieve local version of story //if unable to find, use backend's version. + //this is to prevent any jumps in story after student completes a mission if (SaveManager.hasLocalSave()) { let actionSequence = SaveManager.getLocalSaveData().actionSequence; let story = actionSequence[actionSequence.length - 1].storyID; @@ -149,7 +150,6 @@ function organiseMissions(missions) { } let predicate; const now = new Date(getSessionData().currentDate); - if (isStudent()) { predicate = (mission) => mission.isPublished && isWithinDates(mission, now); @@ -169,9 +169,12 @@ function organiseMissions(missions) { return toPass; } } - missions = missions.filter(predicate); - missions = missions.sort(compareMissions); - return missions; + const sorted_missions = missions.sort(compareMissions); + const remaining_missions = sorted_missions.filter(predicate); + //if no more remaining missions, use the last remaining mission. + return remaining_missions.length > 0 + ? remaining_missions + : [sorted_missions[sorted_missions.length - 1]]; } /** @@ -180,6 +183,10 @@ function organiseMissions(missions) { * global list of open missions, then the corresponding upper (or lower) bound will be used. */ function getMissionPointer(missions, callback) { + // in the scenario with no missions + if (missions == undefined) { + return; + } //finds the mission id's mission pointer let studentStory = getStudentStory(); const isStoryEmpty = story => story === undefined || story.length === 0; From ae15dc12c9843bf69597bb099732cca59a0ca95e Mon Sep 17 00:00:00 2001 From: travisryte Date: Sat, 9 May 2020 01:15:39 +0800 Subject: [PATCH 35/39] Point hosts back to default --- src/components/academy/game/backend/hosting.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/academy/game/backend/hosting.js b/src/components/academy/game/backend/hosting.js index 8b42f6ff29..ba841ea7f2 100644 --- a/src/components/academy/game/backend/hosting.js +++ b/src/components/academy/game/backend/hosting.js @@ -2,7 +2,7 @@ import { isStudent } from './user'; const LIVE_ASSETS_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/'; -export const TEST_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/'; -export const LIVE_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/test-gamedev-bucket/live-stories/'; +export const TEST_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/stories/'; //this should be your materials folder +export const LIVE_STORIES_HOST = 'https://s3-ap-southeast-1.amazonaws.com/source-academy-assets/stories/'; export const ASSETS_HOST = LIVE_ASSETS_HOST; \ No newline at end of file From 9cbfff1412b34a42eab660c8eda12ba3f0afc1eb Mon Sep 17 00:00:00 2001 From: travisryte Date: Sat, 9 May 2020 01:15:54 +0800 Subject: [PATCH 36/39] Removed console.log function --- src/components/academy/game/backend/game-state.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/academy/game/backend/game-state.js b/src/components/academy/game/backend/game-state.js index 9dade53361..78f5ec381a 100644 --- a/src/components/academy/game/backend/game-state.js +++ b/src/components/academy/game/backend/game-state.js @@ -204,6 +204,5 @@ function getMissionPointer(missions, callback) { if (missionPointer === undefined) { missionPointer = missions[0]; } - console.log("Now loading story " + missionPointer.story); // debug statement callback(missionPointer.story); } \ No newline at end of file From 123ef6f301ac4f1ad33b9b4a5c243f65c55a11ab Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 20 May 2020 12:09:43 +0800 Subject: [PATCH 37/39] Fix bugs in backend.ts --- src/sagas/backend.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index 4a41c72130..1a140d7770 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -596,7 +596,6 @@ function* backendSaga(): SagaIterator { yield put(actions.updateGroupOverviews(groupOverviews)); } }); -} yield takeEvery(actionTypes.CHANGE_DATE_ASSESSMENT, function*( action: ReturnType From 6dfd85d7c9d6a6478c228153579901a06eab9fa6 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 20 May 2020 12:27:57 +0800 Subject: [PATCH 38/39] Prettify code --- src/actions/actionTypes.ts | 1 - src/actions/session.ts | 2 +- src/components/academy/NavigationBar.tsx | 2 +- src/components/academy/index.tsx | 2 +- src/sagas/backend.ts | 2 +- src/sagas/requests.ts | 3 --- 6 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index 92cf3702bb..116341844e 100755 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -125,7 +125,6 @@ export const ACKNOWLEDGE_NOTIFICATIONS = 'ACKNOWLEDGE_NOTIFICATIONS'; export const UPDATE_NOTIFICATIONS = 'UPDATE_NOTIFICATIONS'; export const NOTIFY_CHATKIT_USERS = 'NOTIFY_CHATKIT_USERS'; - /** GAMEDEV */ export const FETCH_TEST_STORIES = 'FETCH_TEST_STORIES'; export const SAVE_USER_STATE = 'SAVE_USER_STATE'; diff --git a/src/actions/session.ts b/src/actions/session.ts index 540518b690..36d1b1ae8e 100755 --- a/src/actions/session.ts +++ b/src/actions/session.ts @@ -47,7 +47,7 @@ export const setTokens = ({ export const setUser = (user: { name: string; - role: Role; + role: Role; group: string | null; grade: number; story?: Story; diff --git a/src/components/academy/NavigationBar.tsx b/src/components/academy/NavigationBar.tsx index bc7e82bb2f..68af3b0b4b 100644 --- a/src/components/academy/NavigationBar.tsx +++ b/src/components/academy/NavigationBar.tsx @@ -93,7 +93,7 @@ const NavigationBar: React.SFC = props => (
Ground Control
- + { const resp = await request('groups', 'GET', { accessToken: tokens.accessToken, @@ -687,7 +685,6 @@ export async function getGroupOverviews(tokens: Tokens): Promise Date: Wed, 20 May 2020 12:37:47 +0800 Subject: [PATCH 39/39] Fix bugs in backend.ts --- src/sagas/backend.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index d869b4647b..142c1866fa 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -3,6 +3,7 @@ import { SagaIterator } from 'redux-saga'; import { call, put, select, takeEvery } from 'redux-saga/effects'; +import { MaterialData } from 'src/components/game-dev/storyShape'; import * as actions from '../actions'; import * as actionTypes from '../actions/actionTypes'; import { WorkspaceLocation } from '../actions/workspaces'; @@ -21,7 +22,7 @@ import { Notification, NotificationFilterFunction } from '../components/notification/notificationShape'; -import { IState, Role } from '../reducers/states'; +import { GameState, IState, Role } from '../reducers/states'; import { history } from '../utils/history'; import { showSuccessMessage, showWarningMessage } from '../utils/notification'; import * as request from './requests';