diff --git a/build_propc.py b/build_propc.py deleted file mode 100644 index f305044a..00000000 --- a/build_propc.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/python -# Compresses the Propeller C BlocklyProp generator files into a -# single JavaScript file. -# -# Copyright 2012 Google Inc. -# http://blockly.googlecode.com/ -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script generates two files: -# propc_js_compressed.js -# propc_js_uncompressed.js -# The compressed file is a concatenation of all of Blockly's core files which -# have been run through Google's Closure Compiler. This is done using the -# online API (which takes a few seconds and requires an Internet connection). -# The uncompressed file is a script that loads in each of Blockly's core files -# one by one. This takes much longer for a browser to load, but may be useful -# when debugging code since line numbers are meaningful and variables haven't -# been renamed. The uncompressed file also allows for a faster development -# cycle since there is no need to rebuild or recompile, just reload. - -import httplib, json, urllib, sys - -filenames_libs = [ - "lib/chartist.min.js", - "lib/jquery-1.11.3.min.js", - "lib/beautify.js", - "lib/bootstrap/core/js/bootstrap.min.js", - "lib/bootstrap/plugins/bootbox.min.js" -] - -filenames_setups = [ - "propterm.js", - "blocklypropclient.js", - "blocklyc.js", - "utils.js", - "editor.js" -] - -filenames_cores = [ - "blockly/apps/blockly_compressed.js", - "blockly/generators/propc.js", - "blockly/generators/field_range.js", - "blockly/language/en/messages.js", - "blockly/generators/propcToolbox.js" -] - -filenames_gens = [ - "blockly/generators/propc/base.js", - "blockly/generators/propc/control.js", - "blockly/generators/propc/variables.js", - "blockly/generators/propc/procedures.js", - "blockly/generators/propc/gpio.js", - "blockly/generators/propc/communicate.js", - "blockly/generators/propc/sensors.js", - "blockly/generators/propc/heb.js", - "blockly/generators/propc/s3.js" -] - -filenames_styles = [ - "lib/chartist.min.css", - "lib/bootstrap/core/css/bootstrap.min.css", - "style-editor.css", - "style-clientdownload.css", -] - -header = ('// Do not edit this file; automatically generated by build_propc.py.\n' - '"use strict";') - -def gen_compressed_css(filenames, target_filename): - inc = '' - for filename in filenames: - f = open(filename) - inc = inc + f.read() - f.close() - - inc = inc.replace("url('images", "url('../images") - - params = [('input', inc)] - - - headers = { "Content-type": "application/x-www-form-urlencoded" } - conn = httplib.HTTPSConnection('cssminifier.com') - conn.request('POST', '/raw', urllib.urlencode(params), headers) - response = conn.getresponse() - res = response.read() - conn.close - - f = open(target_filename, 'w') - f.write(res) - f.close() - - original_kb = int(len(inc) / 1024 + 0.5) - compressed_kb = int(len(res) / 1024 + 0.5) - ratio = int(float(compressed_kb) / float(original_kb) * 100 + 0.5) - print 'SUCCESS: ' + target_filename - print 'Size changed from %d KB to %d KB (%d%%).' % (original_kb, compressed_kb, ratio) - - -def gen_uncompressed(filenames, target_filename): - #target_filename = 'propc_js_uncompressed.js' - inc = '' - - for filename in filenames: - f = open(filename) - inc = inc + f.read() + '\n' - f.close() - - - f = open(target_filename, 'w') - f.write(inc) - f.close() - print 'SUCCESS: ' + target_filename - -def gen_compressed(filenames, target_filename): - #target_filename = 'propc_js_compressed.js' - # Define the parameters for the POST request. - params = [ - ('compilation_level', 'SIMPLE_OPTIMIZATIONS'), - ('use_closure_library', 'true'), - ('output_format', 'json'), - ('output_info', 'compiled_code'), - ('output_info', 'warnings'), - ('output_info', 'errors'), - ('output_info', 'statistics'), - ] - - # Read in all the source files. - for filename in filenames: - f = open(filename) - params.append(('js_code', ''.join(f.readlines()))) - f.close() - - # Send the request to Google. - headers = { "Content-type": "application/x-www-form-urlencoded" } - conn = httplib.HTTPSConnection('closure-compiler.appspot.com') - conn.request('POST', '/compile', urllib.urlencode(params), headers) - response = conn.getresponse() - json_str = response.read() - conn.close - - # Parse the JSON response. - json_data = json.loads(json_str) - - def file_lookup(name): - if not name.startswith('Input_'): - return '???' - n = int(name[6:]) - return filenames[n] - - if json_data.has_key('errors'): - errors = json_data['errors'] - for error in errors: - print 'FATAL ERROR' - print error['error'] - print '%s at line %d:' % ( - file_lookup(error['file']), error['lineno']) - print error['line'] - print (' ' * error['charno']) + '^' - else: - if json_data.has_key('warnings'): - warnings = json_data['warnings'] - for warning in warnings: - print 'WARNING' - print warning['warning'] - print '%s at line %d:' % ( - file_lookup(warning['file']), warning['lineno']) - print warning['line'] - print (' ' * warning['charno']) + '^' - print - - code = header + '\n' + json_data['compiledCode'] - - stats = json_data['statistics'] - original_b = stats['originalSize'] - compressed_b = stats['compressedSize'] - if original_b > 0 and compressed_b > 0: - f = open(target_filename, 'w') - f.write(code) - f.close() - - original_kb = int(original_b / 1024 + 0.5) - compressed_kb = int(compressed_b / 1024 + 0.5) - ratio = int(float(compressed_b) / float(original_b) * 100 + 0.5) - print 'SUCCESS: ' + target_filename - print 'Size changed from %d KB to %d KB (%d%%).' % ( - original_kb, compressed_kb, ratio) - else: - print 'UNKNOWN ERROR' - -if __name__ == '__main__': - gen_uncompressed(filenames_cores, 'compressed/propc_cores.js') - gen_uncompressed(filenames_libs, 'compressed/propc_libs.js') - gen_compressed(filenames_setups, 'compressed/propc_setups.min.js') - gen_compressed(filenames_gens, 'compressed/propc_gens.min.js') - gen_compressed_css(filenames_styles, 'compressed/propc_styles.css') \ No newline at end of file diff --git a/src/modules/blocklyc.js b/src/modules/blocklyc.js index 29e7973f..cbd47fd2 100644 --- a/src/modules/blocklyc.js +++ b/src/modules/blocklyc.js @@ -30,7 +30,6 @@ import {loadToolbox} from './editor'; import {CodeEditor, getSourceEditor} from './code_editor'; import {getProjectInitialState, Project} from './project'; import {cloudCompile} from './compiler'; -import {showCannotCompileEmptyProject, showCompilerStatusWindow} from './modals'; import { delay, logConsoleMessage, getURLParameter, sanitizeFilename, utils, prettyCode} from './utility'; @@ -174,6 +173,29 @@ const graph_data = { ], }; + +/** + * Display a dialog window that warns of an attempt to compile + * an empty project. + * + * @param {string} title is the text to include in the dialog title bar + * @param {string} body is the text that is displayed in the body of the dialog + */ +const showCannotCompileEmptyProject = (title, body) => { + utils.showMessage(title, body); +}; + + +/** + * Display the compiler status modal window + * @param {string} titleBar is the text that will appear in the modal title bar + */ +const showCompilerStatusWindow = (titleBar) => { + $('#compile-dialog-title').text(titleBar); + $('#compile-console').val('Compile... '); + $('#compile-dialog').modal('show'); +}; + /** * This is the onClick event handler for the compile toolbar button */ diff --git a/src/modules/client_connection.js b/src/modules/client_connection.js index 86b47026..2b42a22c 100644 --- a/src/modules/client_connection.js +++ b/src/modules/client_connection.js @@ -313,7 +313,7 @@ function wsProcessUiCommand(message) { break; case WS_ACTION_CLOSE_COMPILE: - $('#compile-dialog').modal('hide'); + hideCompilerStatusWindow(); $('#compile-console').val(''); break; @@ -560,3 +560,13 @@ export const getComPort = function() { return commPortSelection; } }; + +/** + * Close the compile progress window + */ +const hideCompilerStatusWindow = () => { + console.log('Hide Compiler dialog window.'); + const dialog = document.getElementById('compile-dialog'); + dialog.modal('hide'); +}; + diff --git a/src/modules/client_service.js b/src/modules/client_service.js index 0645650e..e919f8ee 100644 --- a/src/modules/client_service.js +++ b/src/modules/client_service.js @@ -265,12 +265,7 @@ export const clientService = { */ setSelectedPort: function(portName) { // Do not select the 'Searching...' as a port - if (portName.startsWith('Search')) { - console.log(`Selecting a port while searching.`); - return; - } - - if (this.activeConnection) { + if (!portName.startsWith('Search') && this.activeConnection) { if (portName !== this.getSelectedPort()) { this.selectedPort_ = portName; // Notify Launcher of the selected port diff --git a/src/modules/constants.js b/src/modules/constants.js index 800f00a7..dbecd7a2 100644 --- a/src/modules/constants.js +++ b/src/modules/constants.js @@ -44,14 +44,14 @@ export const EnableSentry = true; * {b#} is the beta release number. * {rc#} is the release candidate number. */ -export const APP_VERSION = '1.5.13'; +export const APP_VERSION = '1.6.0'; /** * Incremental build number. This gets updated before any release * to QA or production. * @type {string} */ -export const APP_BUILD = '220'; +export const APP_BUILD = '221'; /** * Development build stage designator diff --git a/src/modules/dialogs/edit_project.js b/src/modules/dialogs/edit_project.js new file mode 100644 index 00000000..6ad0db8d --- /dev/null +++ b/src/modules/dialogs/edit_project.js @@ -0,0 +1,234 @@ +/* + * TERMS OF USE: MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +import 'jquery-validation'; +import {logConsoleMessage} from '../utility'; +import {getProjectInitialState} from '../project'; +import {populateProjectBoardTypesUIElement} from './new_project'; +import {displayProjectBoardIcon, displayProjectName} from '../editor'; + +/** + * Edit project dialog controller + * @type {{isEventHandler: boolean}} + */ +export const editProjectDialog = { + /** + * Are the dialog event handlers initialized + * @type {boolean} is true if the initializer has been called otherwise false + */ + isEventHandler: false, + + /** + * Set up the event callbacks for the open project dialog + */ + initEventHandlers: function() { + if (this.isEventHandler) { + logConsoleMessage(`Open Project dialog handlers already initialized`); + return; + } + + // Set up element event handlers + this.setContinueHandler(); + this.setCancelHandler(); + this.setEnterHandler(); + + // Record that the event handlers have been installed so subsequent attempts to + // do this again will not cause multiple handlers for the same event from being + // installed. + this.isEventHandler = true; + }, + + /** + * Edit Project Details - Continue button onClick event handler + */ + setContinueHandler: function() { + if (! this.isEventHandler) { + document.getElementById('edit-project-continue').addEventListener('click', function() { + // verify that the project contains a valid board type and project name + if (editProjectDialog.validateEditProjectForm()) { + // Hide the Edit Project modal dialog + $('#edit-project-dialog').modal('hide'); + + editProjectDialog.updateProjectDetails(); + } + }); + } + }, + + /** + * Handle the Enter key press when processing a form + */ + setEnterHandler: function() { + if (! this.isEventHandler) { + // Ignore key pressed in a form field + document.getElementById('edit-project-dialog').addEventListener('keydown', (e) => { + // Let it go if the user is in the description textarea + if (document.activeElement.id === 'edit-project-description') { + return; + } + + if (e.key === 'Enter') { + if (!editProjectDialog.validateEditProjectForm()) { + e.preventDefault(); + // eslint-disable-next-line no-invalid-this + $(this).trigger('submit'); + } else { + $('#new-project-dialog').modal('hide'); + // Update project details. + editProjectDialog.updateProjectDetails(); + } + } + }); + } + }, + + + /** + * Edit Project Details - Cancel button onClick event handler + */ + setCancelHandler: function() { + if (! this.isEventHandler) { + $('#edit-project-cancel').on('click', () => { + // if the project is being edited, clear the fields and close the modal + $('#edit-project-board-dropdown').removeClass('hidden'); + $('#edit-project-details-ro').addClass('hidden'); + $('#edit-project-board-type-select').val(''); + + $('#edit-project-board-type-ro').html(''); + $('#edit-project-created-date-ro').html(''); + $('#edit-project-last-modified-ro').html(''); + + // Hide the Edit Project modal dialog + $('#edit-project-dialog').modal('hide'); + }); + } + }, + + /** + * Present the Edit Project Details modal dialog box + * + * The onClick event handlers for the Cancel and Continue buttons + * will manage the project state as required. + */ + editProjectDetails: function() { + const project = getProjectInitialState(); + this.initEventHandlers(); + + // Load the current project details into the html form data + document.getElementById('edit-project-name').value = project.name; + document.getElementById('edit-project-description').value = project.description; + + populateProjectBoardTypesUIElement( + // document.getElementById('edit-board-type'), + $('#edit-board-type'), + project.boardType.name.toUpperCase()); + + // Display the project create and last modified time stamps + const createDate = project.getCreated(); + const modifiedDate = new Date(project.modified); + + document.getElementById('edit-project-created-date-ro') + .innerHTML = createDate.toLocaleDateString(); + + document.getElementById('edit-project-last-modified-ro') + .innerHTML = modifiedDate.toLocaleDateString(); + + // Reveal the read-only elements + document.getElementById('edit-project-details-ro').classList.remove('hidden'); + + // Show the dialog + $('#edit-project-dialog').modal({ + keyboard: false, + backdrop: 'static', + }); + }, + + /** + * Validate the required elements of the edit project form + * + * @return {boolean} + */ + validateEditProjectForm: function() { + // Select the form element + const projectElement = $('#edit-project-form'); + + // Validate the jQuery object based on these rules. Supply helpful + // error messages to use when a rule is violated + projectElement.validate({ + rules: { + 'edit-project-name': 'required', + 'edit-project-board-type': 'required', + }, + messages: { + 'edit-project-name': 'Please enter a project name', + 'edit-project-board-type': 'Please select a board type', + }, + }); + + return !!projectElement.valid(); + }, + + /** + * Update the name and description details of the current project from the + * DOM elements for those fields + */ + updateProjectDetails: function() { + this.updateProjectNameDescription( + $('#edit-project-name').val(), + $('#edit-project-description').val(), + ); + + this.updateProjectBoardType($('#edit-board-type').val()); + }, + + + /** + * Update the project object name and description + * @param {string} newName + * @param {string} newDescription + */ + updateProjectNameDescription: (newName, newDescription) => { + const project = getProjectInitialState(); + + if (!(project.name === newName)) { + project.name = newName; + displayProjectName(project.name); + } + + if (!(project.description === newDescription)) { + project.description = newDescription; + } + }, + + /** + * Change the project board type if the passed value is different than the current board type + * + * @param {string} board + */ + updateProjectBoardType: (board) => { + const project = getProjectInitialState(); + if (project.getBoardName() !== board) { + // Change the board type + project.setBoardType(board); + displayProjectBoardIcon(board); + } + }, +}; diff --git a/src/modules/dialogs/new_project.js b/src/modules/dialogs/new_project.js index ac3788d5..16b47a99 100644 --- a/src/modules/dialogs/new_project.js +++ b/src/modules/dialogs/new_project.js @@ -217,10 +217,6 @@ export function populateProjectBoardTypesUIElement(element, selected) { return; } - if (selected) { - logConsoleMessage(`Current board type is "${selected}`); - } - // Clear out the board type dropdown menu const length = element[0].options.length; for (let i = length - 1; i >= 0; i--) { diff --git a/src/modules/dialogs/open_project.js b/src/modules/dialogs/open_project.js index ecf59b74..737795c8 100644 --- a/src/modules/dialogs/open_project.js +++ b/src/modules/dialogs/open_project.js @@ -249,7 +249,9 @@ async function selectProjectFile(event) { const /** @type module:project_io.ProjectLoadResult */ result = await loadProjectFile(event.target.files); + if ((! result) || (result.status !== 0)) { + console.log(`SelectProjectFile: Load project failed.`); return Promise.reject(result.message); } @@ -294,13 +296,22 @@ function installOpenProjectModalOpenClick() { closeDialogWindow(); // Copy the stored temp project to the stored local project - const projectJson = window.localStorage.getItem(TEMP_PROJECT_STORE_NAME); - if (projectJson) { - const project = projectJsonFactory(JSON.parse(projectJson)); - if (project) { - insertProject(project); - return; + try { + const projectJson = window.localStorage.getItem(TEMP_PROJECT_STORE_NAME); + if (projectJson) { + const project = projectJsonFactory(JSON.parse(projectJson)); + if (project) { + insertProject(project); + return; + } } + } catch (error) { + utils.showMessage( + `Project Load Error`, + `Unable to load the project`, + () => { + logConsoleMessage(`${error.message}`); + }); } logConsoleMessage('The opened project cannot be found in storage.'); diff --git a/src/modules/editor.js b/src/modules/editor.js index 53cfe073..244ee301 100644 --- a/src/modules/editor.js +++ b/src/modules/editor.js @@ -58,13 +58,14 @@ import {PROJECT_NAME_MAX_LENGTH} from './constants'; import {PROJECT_NAME_DISPLAY_MAX_LENGTH, ApplicationName} from './constants'; import {TestApplicationName, productBannerHostTrigger} from './constants'; import {CodeEditor, propcAsBlocksXml, getSourceEditor} from './code_editor.js'; -import {editProjectDetails} from './modals'; +import {editProjectDialog} from './dialogs/edit_project'; +// import {editProjectDetails} from './modals'; import {NudgeTimer} from './nudge_timer'; import {Project, getProjectInitialState, getDefaultProfile} from './project'; import {setProjectInitialState, setDefaultProfile} from './project'; import {ProjectTypes, clearProjectInitialState} from './project'; import {projectJsonFactory} from './project'; -import {buildDefaultProject} from './project_default'; +import {buildDefaultProject} from './project/project_default'; import {propToolbarButtonController} from './toolbar_controller'; import {filterToolbox} from './toolbox_data'; import {isExperimental} from './url_parameters'; @@ -273,7 +274,13 @@ function initEventHandlers() { // -------------------------------- // Edit project details - $('#edit-project-details').on('click', () => editProjectDetails()); + document.getElementById('edit-project-details').addEventListener('click', () => { + editProjectDialog.editProjectDetails(); + }); + + // $('#edit-project-details').on('click', () => { + // editProjectDialog.editProjectDetails(); + // }); // Help and Reference - online help web pages // Implemented as an href in the menu @@ -549,25 +556,12 @@ function initDefaultProject() { clientService.setTerminalBaudRate(myProject.boardType.baudrate); } - // Create a new nudge timer - const myTime = new NudgeTimer(0); - // Set the callback - myTime.myCallback = function() { - if (isProjectChanged) { - showProjectTimerModalDialog(); - } - }; - - // Start the timer and save it to the project object - myTime.start(10); - defaultProject.setProjectTimer(myTime); - setupWorkspace(defaultProject); - // Create an instance of the CodeEditor class codeEditor = new CodeEditor(defaultProject.boardType.name); if (!codeEditor) { console.log('Error allocating CodeEditor object'); } + setupWorkspace(defaultProject); propToolbarButtonController(); } @@ -1024,7 +1018,7 @@ function generateSvgFooter( project ) { svgFooter += ''; + project.getCreated() + '" data-lastmodified="' + dt + '">'; return svgFooter; } diff --git a/src/modules/modals.js b/src/modules/modals.js deleted file mode 100644 index 80b54750..00000000 --- a/src/modules/modals.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * TERMS OF USE: MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - - -import 'bootstrap/js/modal'; -import 'jquery-validation'; -import {displayProjectName, displayProjectBoardIcon} from './editor.js'; -import {getProjectInitialState} from './project'; -import {utils} from './utility'; -import {populateProjectBoardTypesUIElement} from './dialogs/new_project'; - -/** - * Validate the required elements of the edit project form - * - * @return {boolean} - */ -function validateEditProjectForm() { - // Select the form element - const projectElement = $('#edit-project-form'); - - // Validate the jQuery object based on these rules. Supply helpful - // error messages to use when a rule is violated - projectElement.validate({ - rules: { - 'edit-project-name': 'required', - 'edit-project-board-type': 'required', - }, - messages: { - 'edit-project-name': 'Please enter a project name', - 'edit-project-board-type': 'Please select a board type', - }, - }); - - return !!projectElement.valid(); -} - -/** - * Present the Edit Project Details modal dialog box - * - * The onClick event handlers for the Cancel and Continue buttons - * will manage the project state as required. - */ -export function editProjectDetails() { - const project = getProjectInitialState(); - - // Set the dialog buttons click event handlers - setEditOfflineProjectDetailsContinueHandler(); - setEditOfflineProjectDetailsCancelHandler(); - setEditOfflineProjectDetailsEnterHandler(); - - // Load the current project details into the html form data - $('#edit-project-name').val(project.name); - $('#edit-project-description').val(project.description); - - // Display project board type. - const elementProjectBoardType = $('#edit-project-board-type-ro'); - elementProjectBoardType.html(project.boardType.name.toUpperCase()); - - populateProjectBoardTypesUIElement( - $('#edit-board-type'), - project.boardType.name.toUpperCase()); - - // Display the project create and last modified time stamps - const createDate = new Date(project.created); - const modifiedDate = new Date(project.modified); - $('#edit-project-created-date-ro').html(createDate.toLocaleDateString()); - $('#edit-project-last-modified-ro').html(modifiedDate.toLocaleDateString()); - - // Show the dialog - $('#edit-project-dialog').modal({ - keyboard: false, - backdrop: 'static', - }); -} - -/** - * Handle the Enter key press when processing a form - */ -function setEditOfflineProjectDetailsEnterHandler() { - // Ignore key pressed in a form field - $('#edit-project-dialog').on('keydown', (e) => { - // Let it go if the user is in the description textarea - if (document.activeElement.id === 'edit-project-description') { - return; - } - - if (e.key === 'Enter') { - if (!validateEditProjectForm()) { - e.preventDefault(); - // eslint-disable-next-line no-invalid-this - $(this).trigger('submit'); - } else { - $('#new-project-dialog').modal('hide'); - // Update project details. - updateProjectDetails(); - } - } - }); -} - -/** - * Edit Project Details - Continue button onClick event handler - */ -function setEditOfflineProjectDetailsContinueHandler() { - $('#edit-project-continue').on('click', function() { - // verify that the project contains a valid board type and project name - if (validateEditProjectForm()) { - // Hide the Edit Project modal dialog - $('#edit-project-dialog').modal('hide'); - - updateProjectDetails(); - } - }); -} - -/** - * Update the name and description details of the current project from the - * DOM elements for those fields - */ -function updateProjectDetails() { - updateProjectNameDescription( - $('#edit-project-name').val(), - $('#edit-project-description').val(), - ); - updateProjectBoardType($('#edit-board-type').val()); -} - -/** - * Update the project object name and description - * @param {string} newName - * @param {string} newDescription - */ -function updateProjectNameDescription(newName, newDescription) { - const project = getProjectInitialState(); - - if (!(project.name === newName)) { - project.name = newName; - displayProjectName(project.name); - } - - if (!(project.description === newDescription)) { - project.description = newDescription; - } -} - -/** - * Change the project board type if the passed value is different than the current board type - * - * @param {string} board - */ -function updateProjectBoardType(board) { - const project = getProjectInitialState(); - if (project.getBoardName() !== board) { - // Change the board type - project.setBoardType(board); - displayProjectBoardIcon(board); - } -} - -/** - * Edit Project Details - Cancel button onClick event handler - */ -function setEditOfflineProjectDetailsCancelHandler() { - $('#edit-project-cancel').on('click', () => { - // if the project is being edited, clear the fields and close the modal - $('#edit-project-board-dropdown').removeClass('hidden'); - $('#edit-project-details-ro').addClass('hidden'); - $('#edit-project-board-type-select').val(''); - - $('#edit-project-board-type-ro').html(''); - $('#edit-project-created-date-ro').html(''); - $('#edit-project-last-modified-ro').html(''); - - // Hide the Edit Project modal dialog - $('#edit-project-dialog').modal('hide'); - }); -} - -/** - * Display a dialog window that warns of an attempt to compile - * an empty project. - * - * @param {string} title is the text to include in the dialog title bar - * @param {string} body is the text that is displayed in the body of the dialog - */ -export const showCannotCompileEmptyProject = (title, body) => { - utils.showMessage(title, body); -}; - - -/** - * Display the compiler status modal window - * @param {string} titleBar is the text that will appear in the modal title bar - */ -export const showCompilerStatusWindow = (titleBar) => { - $('#compile-dialog-title').text(titleBar); - $('#compile-console').val('Compile... '); - $('#compile-dialog').modal('show'); -}; - -export const hideCompilerStatusWindow = () => { - $('#compile-dialog').hide(); -}; - diff --git a/src/modules/project.js b/src/modules/project.js index 498b817e..df73814f 100644 --- a/src/modules/project.js +++ b/src/modules/project.js @@ -230,11 +230,37 @@ class Project { // This should be a timestamp but is received as a string // TODO: Convert timestamp string to numeric values + + /** + * The date stamp for when the project was created. + * @type {Date} Records the date the project was created. + * @private + * @description The created on date is implemented as a Date + * object. It is the callers responsibility to ensure a date + * string or an epoch number is not used here. + */ + if (!(created instanceof Date)) { + throw Error(`Project created on parameter must be a Date object`); + } this.created = created; + + /** + * The data stamp for when the project was last modified + * @type {Date} + * @description If this value is null, set it to Date.Now. + */ this.modified = modified; + + /** + * Timestamp set when the project is loaded from storage + * @type {number} + */ this.timestamp = timestamp; - // instance properties that are deprecated + // --------------------------------------- // + // instance properties that are deprecated // + // --------------------------------------- // + /** * The unique project ID, used in the BlocklyProp server implementation. * @type {number} @@ -299,6 +325,27 @@ class Project { } } // End of constructor + /** + * Project created date getter + * @return {Date} + */ + getCreated() { + return this.created; + } + + /** + * Project created date setter + * @param {Date} value + * @return {Date} + */ + setCreated(value) { + if ( ! (value instanceof Date)) { + throw Error(`Cannot set Project created on date with "${value}`); + } + this.created = value; + return value; + } + /** * Return the name of the current project board type * @return {string} @@ -634,14 +681,25 @@ function projectJsonFactory(json) { console.log('Unknown board type: %s', json.boardType.name); } + // Check the created on time stamp + let createdOnDate = date; + if (typeof json.created == 'number') { + createdOnDate.setTime(json.created); + } else if (json.created instanceof Date) { + createdOnDate = json.created; + } else { + // Assuming that the value is a string + createdOnDate = new Date(json.created); + } + return new Project( json.name, json.description, tmpBoardType, ProjectTypes.PROPC, json.code, - Date.parse(json.created), - Date.parse(json.modified), + createdOnDate, + (json.modified && json.modified.length > 0) ? Date.parse(json.modified) : date, date.getTime(), ); } diff --git a/src/modules/project_default.js b/src/modules/project/project_default.js similarity index 96% rename from src/modules/project_default.js rename to src/modules/project/project_default.js index cdc85e95..0fdb1524 100644 --- a/src/modules/project_default.js +++ b/src/modules/project/project_default.js @@ -25,7 +25,7 @@ */ -import {Project, ProjectProfiles, ProjectTypes} from './project.js'; +import {Project, ProjectProfiles, ProjectTypes} from '../project.js'; /** * Generate an empty default project diff --git a/src/modules/project_file_validation.js b/src/modules/project/project_file_validation.js similarity index 100% rename from src/modules/project_file_validation.js rename to src/modules/project/project_file_validation.js diff --git a/src/modules/project/project_io.js b/src/modules/project/project_io.js index 41cc34c7..3783e340 100644 --- a/src/modules/project/project_io.js +++ b/src/modules/project/project_io.js @@ -65,11 +65,12 @@ export async function loadProjectFile(files) { // Load the project file into a Blob and return as an XML string const resData = await blobToData(new Blob(fileList, {type: 'text/strings'})); - // Parse the XML string to validate content as a valid project - await parseProjectFileString(fileList[0].name, fileType, resData); + // Parse the XML string to validate content as a valid project. This method will + // throw an exception if anything it examines is incorrect or missing + await validateProjectFileXml(fileList[0].name, fileType, resData); // Convert project XML string to a Project object - const project = filestreamToProject(fileList[0].name, resData); + const project = convertFilestreamToProject(fileList[0].name, resData); if (project) { return formatResult(0, 'success', project); } @@ -88,10 +89,7 @@ export async function loadProjectFile(files) { * without a namespace * @return {Project} */ -export const filestreamToProject = (projectName, rawCode) => { - // TODO: Solo #261 - // validateProjectBlockList(this.result); - +const convertFilestreamToProject = (projectName, rawCode) => { // Search the project file for the first variable or block const codeStartIndex = (rawCode.indexOf(' -1) ? ' { Project.getEmptyProjectCodeHeader() + blockCode + '' : Project.getEmptyProjectCodeHeader() + ''; + // Load the created on and last modified dates. If either date is invalid, + // use a known valid date in it's place to keep everything happy. const date = new Date(); - const projectDesc = getProjectDescriptionFromXML(rawCode); - const projectModified = getProjectModifiedDateFromXML(rawCode, date); - const projectBoardType = getProjectBoardTypeFromXML(rawCode); - let projectNameString = getProjectTitleFromXML(rawCode); + const projectModified = getProjectModifiedDate(rawCode, date); + const projectCreated = getProjectCreatedDate(rawCode, new Date(projectModified)); + + const projectDesc = getProjectDescription(rawCode); + const projectBoardType = getProjectBoardType(rawCode); + let projectNameString = getProjectTitle(rawCode); if (projectNameString === '') { projectNameString = projectName; } - // Project create date can be missing in some projects. Set it to the - // last modify date as a last-ditch default - let projectCreated = getProjectCreatedDateFromXML(rawCode, date); - if (! projectCreated) { - projectCreated = projectModified; - } - try { const tmpBoardType = Project.convertBoardType(projectBoardType); if (tmpBoardType === undefined) { @@ -131,7 +126,7 @@ export const filestreamToProject = (projectName, rawCode) => { return new Project( projectNameString, - decodeFromValidXml(projectDesc), + projectDesc, tmpBoardType, ProjectTypes.PROPC, projectXmlCode, @@ -140,7 +135,7 @@ export const filestreamToProject = (projectName, rawCode) => { date.getTime(), true); } catch (e) { - console.log('Error while creating project object. %s', e.message); + console.log('Error while converting project file stream: %s', e.message); } return null; @@ -152,14 +147,15 @@ export const filestreamToProject = (projectName, rawCode) => { * @param {string} xmlString * @return {string} */ -function getProjectDescriptionFromXML(xmlString) { +function getProjectDescription(xmlString) { const titleIndex = xmlString.indexOf( 'transform="translate(-225,-8)">Description: '); if (titleIndex > -1) { - return xmlString.substring( - (titleIndex + 44), - xmlString.indexOf('', (titleIndex + 44))); + return decodeFromValidXml( + xmlString.substring( + (titleIndex + 44), + xmlString.indexOf('', (titleIndex + 44)))); } return ''; @@ -171,7 +167,7 @@ function getProjectDescriptionFromXML(xmlString) { * @param {string} xml * @return {string} */ -function getProjectBoardTypeFromXML(xml) { +function getProjectBoardType(xml) { // transform=\"translate(-225,-23)\">Device: activity-board const searchString = `transform="translate(-225,-23)">Device: `; const index = xml.indexOf(searchString); @@ -191,7 +187,7 @@ function getProjectBoardTypeFromXML(xml) { * @param {string} xml * @return {string} */ -function getProjectTitleFromXML(xml) { +function getProjectTitle(xml) { const searchString = `transform="translate(-225,-53)">Title: `; const index = xml.indexOf(searchString); @@ -221,32 +217,56 @@ function getProjectTitleFromXML(xml) { } /** - * Parse the xml string to locate and return the project created timestamp + * Parse the xml string to locate and return the project created timestamp. The project created + * date can be missing in some projects. Set the date to the provided value if one is not found + * within the project. * - * @param {string} xmlString - * @param {Date} defaultTimestamp - * @return {string|*} + * @param {string} xmlString Contains the raw project xml. + * @param {Date} defaultTimestamp This value is returned if the xml does not + * contain a "created_on" element. + * @return {Date|*} */ -function getProjectCreatedDateFromXML(xmlString, defaultTimestamp) { +function getProjectCreatedDate(xmlString, defaultTimestamp) { const titleIndex = xmlString.indexOf('data-createdon="'); - if (titleIndex > -1) { - return xmlString.substring( - (titleIndex + 16), - xmlString.indexOf('"', (titleIndex + 17))); + if (titleIndex === -1) { + logConsoleMessage(`Project created date is missing. Setting default`); + return defaultTimestamp; } - return defaultTimestamp; + const start = titleIndex + 16; + const end = titleIndex + 17; + const index = xmlString.indexOf('"', end); + if (index === -1) { + return defaultTimestamp; + } + + const result = xmlString.substring(start, index); + if (result === 'NaN') { + return defaultTimestamp; + } + + let convertedDate; + if (result.length !== 13) { + convertedDate = new Date(result); + } else { + convertedDate = new Date(0); + const numDate = parseInt(result, 10); + convertedDate.setTime(numDate); + } + + return convertedDate; } /** - * Parse the xml string to locate and return the project last modified timestamp + * Parse the xml string to locate and return the project last modified timestamp. Use the provided + * default value if the last modified date in the project is not found or is invalid. * * @param {string} xmlString * @param {Date} defaultTimestamp * @return {string|*} */ -function getProjectModifiedDateFromXML(xmlString, defaultTimestamp) { +function getProjectModifiedDate(xmlString, defaultTimestamp) { const titleIndex = xmlString.indexOf('data-lastmodified="'); if (titleIndex > -1) { @@ -254,6 +274,7 @@ function getProjectModifiedDateFromXML(xmlString, defaultTimestamp) { (titleIndex + 19), xmlString.indexOf('"', (titleIndex + 20))); } else { + console.log('Setting project last modified date to now.'); return defaultTimestamp; } } @@ -319,7 +340,7 @@ function formatResult(status, message, project) { * @param {string} xmlString * @throws StringException */ -async function parseProjectFileString(filename, fileType, xmlString) { +async function validateProjectFileXml(filename, fileType, xmlString) { // The project board type string const uploadBoardType = getProjectBoardTypeName(xmlString); if (uploadBoardType.length === 0) { @@ -328,15 +349,6 @@ async function parseProjectFileString(filename, fileType, xmlString) { 'Project board type was not found'); } - // The text name of the project - // const projectName = filename.substring(0, filename.lastIndexOf('.')); - // logConsoleMessage(`Loading project :=> ${projectName}`); - - // TODO: Solo #261 - // Loop through blocks to verify blocks are supported for the project - // board type - // validateProjectBlockList(this.result); - const isSvgFile = (fileType === 'image/svg+xml'); // Flag to indicate that we are importing a file that was exported from the diff --git a/src/templates/index.html b/src/templates/index.html index 6a8f2e94..9429bb80 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -609,10 +609,6 @@