diff --git a/lib/editor/components/timetable/EditableCell.js b/lib/editor/components/timetable/EditableCell.js index 2f23f3cb2..6f0e8624c 100644 --- a/lib/editor/components/timetable/EditableCell.js +++ b/lib/editor/components/timetable/EditableCell.js @@ -5,7 +5,7 @@ import moment from 'moment' import * as tripActions from '../../actions/trip' import {secondsAfterMidnightToHHMM} from '../../../common/util/gtfs' -import { isTimeFormat } from '../../util/timetable' +import { isTimeFormat, parseCellValue } from '../../util/timetable' import type {TimetableColumn} from '../../../types' import type {EditorValidationIssue} from '../../util/validation' @@ -66,8 +66,6 @@ export default class EditableCell extends Component { }) } - cellInput = null - /** * The component may receive data from a save event or * editing can be changed by a change in the activeCell in the TimetableGrid @@ -157,8 +155,6 @@ export default class EditableCell extends Component { offsetScrollCol(-1) return case 38: // up - this.save() - break case 40: // down this.save() break @@ -196,6 +192,7 @@ export default class EditableCell extends Component { } save () { + this.cancel() const {column} = this.props const {data} = this.state // for non-time rendering @@ -206,6 +203,14 @@ export default class EditableCell extends Component { // Ensure that only a positive integer value can be set. const value = +data this._handleSave(value) + } else if (isTimeFormat(column.type)) { + if (typeof data !== 'string') return this.cancel() + const parsedValue = parseCellValue(data, column) + if (parsedValue !== null) { + this._handleSave(parsedValue) + } else { + this.cancel() + } } else { if (typeof data !== 'string') return this.cancel() const duration = moment.duration(data) @@ -238,11 +243,8 @@ export default class EditableCell extends Component { ? String.fromCharCode(13) : undefined const rows = text.split(rowDelimiter) - const rowsAndColumns = [] - // Split each row into columns - for (let i = 0; i < rows.length; i++) { - rowsAndColumns.push(rows[i].split(String.fromCharCode(9))) - } + // Remove carriage return characters (/r) from rows to handle pasted data from Excel + const rowsAndColumns = rows.map(row => row.split(String.fromCharCode(9)).map(cellValue => cellValue.replace(/\r/g, ''))) if (rowsAndColumns.length > 1 || rowsAndColumns[0].length > 1) { this.cancel() diff --git a/lib/editor/components/timetable/TimetableGrid.js b/lib/editor/components/timetable/TimetableGrid.js index 916105650..bbe2e7340 100644 --- a/lib/editor/components/timetable/TimetableGrid.js +++ b/lib/editor/components/timetable/TimetableGrid.js @@ -10,8 +10,6 @@ import objectPath from 'object-path' import * as tripActions from '../../actions/trip' import {ENTITY} from '../../constants' -import HeaderCell from './HeaderCell' -import EditableCell from './EditableCell' import { getHeaderColumns, HEADER_GRID_STYLE, @@ -23,17 +21,19 @@ import { MAIN_GRID_WRAPPER_STYLE, OVERSCAN_COLUMN_COUNT, OVERSCAN_ROW_COUNT, - parseTime, + parseCellValue, ROW_HEIGHT, SCROLL_SIZE, TOP_LEFT_STYLE, WRAPPER_STYLE } from '../../util/timetable' - import type {Pattern, TimetableColumn} from '../../../types' import type {TimetableState} from '../../../types/reducers' import type {TripValidationIssues} from '../../selectors/timetable' +import EditableCell from './EditableCell' +import HeaderCell from './HeaderCell' + type Style = {[string]: number | string} type Props = { @@ -371,38 +371,46 @@ export default class TimetableGrid extends Component { addNewRow, columns, data, - setActiveCell, hideDepartureTimes, updateCellValue, updateScroll } = this.props let activeRow = rowIndex let activeCol = colIndex - // iterate over rows in pasted selection + let errorShown = false + // Iterate over rows in pasted selection for (var i = 0; i < pastedRows.length; i++) { activeRow = rowIndex + i - // construct new row if it doesn't exist - if (typeof data[i + rowIndex] === 'undefined') { + // Construct new row if it doesn't exist + if (typeof data[activeRow] === 'undefined') { addNewRow() } - // iterate over number of columns in pasted selection + // Iterate over number of columns in pasted selection for (var j = 0; j < pastedRows[0].length; j++) { activeCol = colIndex + j - const path = `${rowIndex + i}.${columns[colIndex + j].key}` - const value = parseTime(pastedRows[i][j]) - updateCellValue({value, rowIndex: rowIndex + i, key: path}) - // if departure times are hidden, paste into adjacent time column - const adjacentPath = `${rowIndex + i}.${columns[colIndex + j + 2].key}` - if ( - hideDepartureTimes && - isTimeFormat(columns[colIndex + j].type) && - typeof objectPath.get(data, adjacentPath) !== 'undefined' - ) { - updateCellValue({value, rowIndex: rowIndex + i, key: adjacentPath}) + const col = columns[activeCol] + const path = `${activeRow}.${col.key}` + const originalValue = objectPath.get(data[activeRow], col.key) + const value = parseCellValue(pastedRows[i][j], col, errorShown) + if (value !== null) { + const finalValue = value + updateCellValue({ value: finalValue, rowIndex: activeRow, key: path }) + // If departure times are hidden, paste into adjacent time column + const adjacentPath = `${activeRow}.${columns[activeCol + 2].key}` + if ( + hideDepartureTimes && + isTimeFormat(col.type) && + typeof objectPath.get(data, adjacentPath) !== 'undefined' + ) { + updateCellValue({ value: finalValue, rowIndex: activeRow, key: adjacentPath }) + } + } else { + // If the value is rejected, keep the original value + updateCellValue({ value: originalValue, rowIndex: activeRow, key: path }) + errorShown = true } } } - setActiveCell(`${activeRow}-${activeCol}`) updateScroll(activeRow, activeCol) } diff --git a/lib/editor/util/timetable.js b/lib/editor/util/timetable.js index e184fc206..248276dc6 100644 --- a/lib/editor/util/timetable.js +++ b/lib/editor/util/timetable.js @@ -42,12 +42,17 @@ export function getHeaderColumns ( return columns.filter(c => c.type !== 'DEPARTURE_TIME') } -export function parseTime (timeString: string) { +export function parseCellValue (timeString: string, col: TimetableColumn, errorShown?: boolean) { const date = moment().startOf('day').format('YYYY-MM-DD') - return moment(date + 'T' + timeString, TIMETABLE_FORMATS).diff( - date, - 'seconds' - ) + const parsedDate = moment(date + 'T' + timeString, TIMETABLE_FORMATS, true) + if (isTimeFormat(col.type)) { + if (!parsedDate.isValid()) { + if (errorShown !== true) alert(`Please enter a valid time format`) + return null + } + return moment(date + 'T' + timeString, TIMETABLE_FORMATS).diff(date, 'seconds') + } + return timeString } // Helper function to find and return the active calendar, be it traditional or exception-based.