+ )
+ }
+
+ styles () {
+ return {
+ }
+ }
+}
+
+ValidCheck.propTypes = {
+ // if provided it'll create a label element to accompany the field
+ label: PropTypes.string,
+ // required name for the field
+ name: PropTypes.string.isRequired,
+ // className will set on the containing div
+ className: PropTypes.string,
+ // whether the input starts checked
+ checked: PropTypes.bool,
+ showError: PropTypes.bool,
+ // value to display in the field
+ value: PropTypes.string,
+ // short message to help the user
+ helpText: PropTypes.string,
+ // weather to show help text or not
+ showHelpText: PropTypes.bool,
+ // change handler func. will be called with (name, value, event)
+ onChange: PropTypes.func.isRequired,
+ // show label on top
+ labelTop: PropTypes.bool,
+ // should a "none" option be allowed?
+ allowEmpty: PropTypes.bool
+}
+
+ValidCheck.defaultProps = {
+ name: undefined,
+ showError: true,
+ error: undefined,
+ helpText: '',
+ showHelpText: false,
+ allowEmpty: true
+}
diff --git a/lib/constants/editor.js b/lib/constants/editor.js
index b0d35016..9e59c81f 100644
--- a/lib/constants/editor.js
+++ b/lib/constants/editor.js
@@ -10,3 +10,8 @@ export const EDITOR_SET_VIZ_SCRIPT = 'EDITOR_SET_VIZ_SCRIPT'
export const EDITOR_SET_TRANSFORM_SCRIPT = 'EDITOR_SET_TRANFORM_SCRIPT'
export const EDITOR_UPDATE_BODY = 'EDITOR_UPDATE_BODY'
export const EDITOR_REMOVE_SECTION = 'EDITOR_REMOVE_SECTION'
+export const EDITOR_SET_SCHEMA = 'EDITOR_SET_SCHEMA'
+export const EDITOR_SET_BODY_VIEW = 'EDITOR_SET_BODY_VIEW'
+export const EDITOR_SET_COL_ORDER = 'EDITOR_SET_COL_ORDER'
+export const EDITOR_SET_ROW_ORDER = 'EDITOR_SET_ROW_ORDER'
+export const EDITOR_SET_BODY_ERROR = 'EDITOR_SET_BODY_ERROR'
diff --git a/lib/containers/Editor.js b/lib/containers/Editor.js
index 650556cc..88ca3f6f 100644
--- a/lib/containers/Editor.js
+++ b/lib/containers/Editor.js
@@ -18,9 +18,15 @@ import {
setVizScript,
setBody,
updateBody,
- removeSection
+ removeSection,
+ setSchema,
+ setBodyView,
+ setRowOrder,
+ setColOrder,
+ setBodyError
} from '../actions/editor'
-import { selectDryRunDatasetById } from '../selectors/dataset'
+
+import { selectDryRunDatasetById, extractColumnHeaders } from '../selectors/dataset'
import { selectSessionProfileId, selectSessionProfileHandle } from '../selectors/session'
import Editor from '../components/editor/Editor'
@@ -30,7 +36,19 @@ const EditorContainer = connect(
const id = selectSessionProfileId(state)
const profilePeername = selectSessionProfileHandle(state)
- const { name, dataset, transformScript, vizScript, body } = state.editor
+ const {
+ name,
+ dataset,
+ transformScript,
+ vizScript,
+ body,
+ bodyView,
+ showHeaders,
+ bodyError,
+ colOrder,
+ rowOrder
+ } = state.editor
+
return Object.assign({
profilePeername,
id,
@@ -40,6 +58,12 @@ const EditorContainer = connect(
transformScript,
vizScript,
body,
+ showHeaders,
+ bodyError,
+ bodyView,
+ rowOrder,
+ colOrder,
+ columnHeaders: extractColumnHeaders({ dataset }),
// localDataset: selectLocalDatasetByPath(state, 'new'),
// TODO - finish
@@ -59,6 +83,11 @@ const EditorContainer = connect(
setBody,
updateBody,
removeSection,
+ setSchema,
+ setBodyView,
+ setRowOrder,
+ setColOrder,
+ setBodyError,
dryRunDataset,
saveDataset,
diff --git a/lib/qri/generate.js b/lib/qri/generate.js
new file mode 100644
index 00000000..5258e9c4
--- /dev/null
+++ b/lib/qri/generate.js
@@ -0,0 +1,191 @@
+import cloneDeep from 'clone-deep'
+
+export function generateSchemaFromRow (row) {
+ const items = row.map((e, i) => { return { 'title': 'col_' + (i + 1) } })
+ const schema = {
+ type: 'array',
+ items: {
+ type: 'array',
+ items
+ }
+ }
+ return schema
+}
+
+export function generateCollapsedChanges (body, columnHeaders, colOrder, rowOrder) {
+ if (!rowOrder || !Array.isArray(rowOrder) || !colOrder || !Array.isArray(colOrder) || !body || !columnHeaders) {
+ return
+ }
+ var rowMapped = rowOrder.map((position, index) => { return { index, position } })
+ rowMapped.sort((a, b) => a.position - b.position)
+ var newBody = rowMapped.map(el => body[el.index])
+
+ var colMapped = colOrder.map((position, index) => { return { index, position } })
+ colMapped.sort((a, b) => a.position - b.position)
+
+ const newColumnHeaders = colMapped.map(el => columnHeaders[el.index])
+ newBody = newBody.map(row => colMapped.map(el => row[el.index]))
+
+ const newColOrder = colOrder.map((el, index) => index)
+ const newRowOrder = rowOrder.map((el, index) => index)
+ return {
+ body: newBody,
+ schema: {
+ type: 'array',
+ items: {
+ type: 'array',
+ items: newColumnHeaders
+ }
+ },
+ rowOrder: newRowOrder,
+ colOrder: newColOrder
+ }
+}
+
+// takes a body and structure and returns
+// the most valid version of the parsed body & structure
+// if the body or schema can't be parsed, it returns a string error
+export function generateParsedBodyAndStructure (body, structure, colOrder, rowOrder) {
+ var validBody = body
+ var validStructure = structure
+ var schema = structure && structure.schema
+
+ // will return either parsed schema, undefined, or string error
+ schema = generateParsedSchema(schema)
+ if (typeof schema === 'string') {
+ return schema
+ }
+ validStructure = Object.assign({}, structure, { schema })
+ // time to check the body
+ // it body is a string, parse it!
+ if (typeof body === 'string') {
+ // if the body is empty, don't pass along a body
+ if (body === '') {
+ validBody = undefined
+ } else {
+ try {
+ validBody = JSON.parse(body)
+ } catch (e) {
+ return 'The dataset body is invalid.'
+ }
+ }
+ } else {
+ // otherwise, it is an object. The only case where the body would be an
+ // object, and stored in the dataset editor, is if it is a 2D array
+ // with a schema. Collapse any changes. If there are changes, pass
+ // them to the validBody and validStructure vars
+ const columnHeaders = schema && schema.items && schema.items.items
+ const changes = generateCollapsedChanges(validBody, columnHeaders, colOrder, rowOrder)
+ if (changes) {
+ validBody = changes.body
+ validStructure['schema'] = changes.schema
+ }
+ }
+
+ return { body: validBody, structure: validStructure }
+}
+
+// returns a parsed schema, undefined (if schema was false, undefined or ''), or a string error if the schema cannot be parsed
+export function generateParsedSchema (schema) {
+ if (!schema) {
+ return undefined
+ }
+ if (typeof schema !== 'string') {
+ return schema
+ }
+ try {
+ const parsedSchema = JSON.parse(schema)
+ return parsedSchema
+ } catch (e) {
+ return 'The dataset schema is invalid.'
+ }
+}
+
+// generateMatchingSchemaAndBody returns a schema and body that match.
+// If we are in table view, we return a parsed Body and Schema (as well as
+// fresh col and row orders)
+// If we are in json view, we return a stringified Body, and leave the
+// schema as is
+// returns { schema, body, rowOrder, colOrder }, or a string error
+export function generateMatchingSchemaAndBody (bodyView, structure, prevBody, columnHeaders, colOrder, rowOrder) {
+ var schema = structure && cloneDeep(structure.schema)
+ var body = cloneDeep(prevBody)
+ if (bodyView === 'table') {
+ // case where we have the body as a string, but need it to be
+ // an object
+ if (typeof prevBody === 'string') {
+ try {
+ body = JSON.parse(prevBody)
+ } catch (e) {
+ return "Body is invalid in it's current state."
+ }
+ }
+ // case where we have the body (which is an object)
+ // and we want to display in table view. We need to make sure
+ // it is the correct shape:
+ if (!Array.isArray(body)) {
+ return 'Table view is reserved for two dimentional data, the top level body must be an array.'
+ }
+ const row = body[0]
+ if (body.some(r => !Array.isArray(r) || r.length !== row.length)) {
+ return 'Table view is reserved for two dimentional data. Each element in the body must be an array and must be the same length.'
+ }
+ // From now one we know we have a 2D array!
+ //
+ // parse the schema, if its a string then it will return parsed (or as an error)
+ // if it is already parsed, it will just return
+ schema = generateParsedSchema(schema)
+ //
+ // if a string is returned from generateParsedSchema then it is
+ // actually an error string, just return it.
+ if (typeof schema === 'string') {
+ return schema
+ }
+ //
+ // from here on guarenteed a parsed or undefined schema!
+ if (schema) {
+ // We have a schema!
+ if (!schema.type || schema.type !== 'array' || !schema.items || schema.items.type !== 'array' || !schema.items.items || !Array.isArray(schema.items.items) || schema.items.items.length !== row.length) {
+ return 'The current schema does not match the structure of the body. If you remove the current schema, Qri will generate a basic schema for you'
+ }
+ // case where the body is a 2D array and we have schema that
+ // matches the body!
+ return {
+ schema,
+ colOrder: schema.items.items.map((elem, index) => index),
+ rowOrder: row.map((elem, index) => index),
+ body
+ }
+ }
+ // We have do not have a schema, so we must generate it!
+ schema = generateSchemaFromRow(row)
+ return {
+ schema,
+ colOrder: schema.items.items.map((elem, index) => index),
+ rowOrder: row.map((elem, index) => index),
+ body
+ }
+ }
+ // From here on in, we want the bodyView to be json:
+ // case where we want to have json view, and the body is already a string
+ // we don't have to make any changes
+ if (typeof body === 'string') {
+ return { body }
+ }
+ // case where we are trying to set view to json, and we have a
+ // non-string body
+ // we have to collapse the changes and stringify that (when we no longer use stringify)
+ const changed = generateCollapsedChanges(body, columnHeaders, colOrder, rowOrder)
+ if (changed) {
+ body = JSON.stringify(changed.body, null, 2)
+ return {
+ body,
+ schema: changed.schema,
+ colOrder: changed.colOrder,
+ rowOrder: changed.rowOrder
+ }
+ }
+
+ body = JSON.stringify(body, null, 2)
+ return { body }
+}
diff --git a/lib/reducers/editor.js b/lib/reducers/editor.js
index 7df35ef3..5af9f1b4 100644
--- a/lib/reducers/editor.js
+++ b/lib/reducers/editor.js
@@ -10,9 +10,18 @@ import {
EDITOR_SET_VIZ_SCRIPT,
EDITOR_SET_TRANSFORM,
EDITOR_SET_TRANSFORM_SCRIPT,
- EDITOR_REMOVE_SECTION
+ EDITOR_REMOVE_SECTION,
+ EDITOR_SET_SCHEMA,
+ EDITOR_SET_BODY_VIEW,
+ EDITOR_SET_COL_ORDER,
+ EDITOR_SET_ROW_ORDER,
+ EDITOR_SET_BODY_ERROR
} from '../constants/editor'
+import {
+ generateMatchingSchemaAndBody
+} from '../qri/generate'
+
import cloneDeep from 'clone-deep'
const initialState = {
@@ -21,22 +30,67 @@ const initialState = {
dataset: {},
vizScript: undefined,
transformScript: undefined,
- body: undefined
+ body: undefined,
+ colOrder: undefined,
+ rowOrder: undefined,
+ bodyView: 'json',
+ bodyError: ''
}
// TODO - TESTS!
export default function editorReducer (state = initialState, action) {
switch (action.type) {
case EDITOR_INIT_DATASET:
+ var body = action.body
+ if (body) {
+ body = JSON.stringify(action.body, null, 2)
+ }
return {
dirty: false,
name: action.name || '',
dataset: action.dataset || {},
vizScript: action.vizScript,
transformScript: action.transformScript,
- body: action.body
+ bodyView: 'json',
+ body: body
}
-
+ case EDITOR_SET_BODY_VIEW:
+ var { body: prevBody, columnHeaders, colOrder, rowOrder, dirty } = state
+ var dataset = state.dataset
+ const check = generateMatchingSchemaAndBody(action.view, dataset.structure, prevBody, columnHeaders, colOrder, rowOrder)
+ if (typeof check === 'string') {
+ // if `check` is a string, then we have a problem switching to the other body view
+ // assign the error
+ return Object.assign({}, state, { bodyView: action.view, bodyError: check })
+ }
+ // Otherwise, we might have to update other parts of our statetree
+ // to get the view to show correctly:
+ if (check.body) {
+ dirty = true
+ body = check.body
+ }
+ if (check.schema) {
+ dirty = true
+ var structure = Object.assign({}, dataset.structure, { schema: check.schema })
+ dataset = Object.assign({}, dataset, { structure })
+ }
+ if (check.colOrder) {
+ dirty = true
+ colOrder = check.colOrder
+ }
+ if (check.rowOrder) {
+ dirty = true
+ rowOrder = check.rowOrder
+ }
+ return Object.assign({}, state, {
+ dirty,
+ bodyView: action.view,
+ bodyError: '',
+ body,
+ dataset,
+ colOrder,
+ rowOrder
+ })
case EDITOR_SET_NAME:
return Object.assign({}, state, { dirty: true, name: action.name })
case EDITOR_SET_COMMIT:
@@ -48,8 +102,8 @@ export default function editorReducer (state = initialState, action) {
dataset: Object.assign({}, state.dataset, { meta: action.meta })
})
case EDITOR_SET_STRUCTURE:
- var dataset = state.dataset
- var body = cloneDeep(state.body)
+ dataset = state.dataset
+ body = cloneDeep(state.body)
const prevFormat = state && state.dataset && state.dataset.structure && state.dataset.structure.format
const currFormat = action.structure && action.structure.format
if (prevFormat !== currFormat) {
@@ -64,6 +118,13 @@ export default function editorReducer (state = initialState, action) {
dataset: Object.assign({}, state.dataset, { structure: action.structure }),
body
})
+ case EDITOR_SET_SCHEMA:
+ dataset = state.dataset
+ structure = Object.assign({}, dataset.structure, { schema: action.schema })
+ return Object.assign({}, state, {
+ dirty: true,
+ dataset: Object.assign({}, dataset, { structure })
+ })
case EDITOR_SET_TRANSFORM:
return Object.assign({}, state, { dirty: true,
dataset: Object.assign({}, state.dataset, { transform: action.transform })
@@ -74,21 +135,39 @@ export default function editorReducer (state = initialState, action) {
})
case EDITOR_SET_BODY:
return Object.assign({}, state, { dirty: true, body: action.body })
+ case EDITOR_SET_BODY_ERROR:
+ return Object.assign({}, state, { dirty: true, error: action.error })
case EDITOR_UPDATE_BODY:
var newBody = cloneDeep(state.body)
for (let [row, column, oldValue, newValue] of action.changes) { // eslint-disable-line no-unused-vars
newBody[row][column] = newValue
}
- return Object.assign({}, state, { dirty: true, body: newBody })
+ return Object.assign({}, state, {
+ dirty: true,
+ body: newBody
+ })
case EDITOR_SET_VIZ_SCRIPT:
return Object.assign({}, state, { dirty: true, vizScript: action.vizScript })
case EDITOR_SET_TRANSFORM_SCRIPT:
return Object.assign({}, state, { dirty: true, transformScript: action.transformScript })
case EDITOR_REMOVE_SECTION:
- const datasetSections = Object.keys(state.dataset).filter(s => s !== action.section)
+ const datasetSections = Object.keys(state.dataset).filter(s => {
+ if (action.section === 'body') {
+ return s !== action.section && s !== 'bodyBytes' && s !== 'bodyPath'
+ }
+ return s !== action.section
+ })
+ body = state.body
+ if (action.section === 'body') {
+ body = undefined
+ }
dataset = {}
datasetSections.forEach(s => { dataset[s] = state.dataset[s] })
- return Object.assign({}, state, { dirty: true, dataset: dataset })
+ return Object.assign({}, state, { dirty: true, dataset, body })
+ case EDITOR_SET_COL_ORDER:
+ return Object.assign({}, state, { dirty: true, colOrder: action.order })
+ case EDITOR_SET_ROW_ORDER:
+ return Object.assign({}, state, { dirty: true, rowOrder: action.order })
default:
return state
}
diff --git a/lib/scss/_handsontable.scss b/lib/scss/_handsontable.scss
new file mode 100644
index 00000000..5e026e75
--- /dev/null
+++ b/lib/scss/_handsontable.scss
@@ -0,0 +1,14 @@
+// styling for the HandsOnTableEditor
+
+.editor.handsontable th {
+ color: $primary-light;
+}
+
+// for some reason this specific left side border wants to be a weird color
+// this random css fixes it so it looks like every other border
+.editor.handsontable.htRowHeaders thead tr th:nth-child(2) {
+ border-left: 1px solid #1e1e1e;
+ border-left-width: 1px;
+ border-left-style: solid;
+ border-left-color: rgb(30, 30, 30);
+}
\ No newline at end of file
diff --git a/lib/scss/style.scss b/lib/scss/style.scss
index 007377d8..8cda8361 100755
--- a/lib/scss/style.scss
+++ b/lib/scss/style.scss
@@ -10,4 +10,6 @@
@import "dropdown";
@import "hotable";
-@import "site";
\ No newline at end of file
+@import "site";
+
+@import "handsontable";
\ No newline at end of file
diff --git a/lib/selectors/dataset.js b/lib/selectors/dataset.js
index b6643b73..06fe0f19 100755
--- a/lib/selectors/dataset.js
+++ b/lib/selectors/dataset.js
@@ -136,27 +136,30 @@ export function selectNoDatasetBody (state, section, node) {
selectFetchedAll(state, section, node) === true && (data === [] || data === {})
}
-// extractSchema should be used when trying to get the schema
-// it will extract the column names and details for structured data
-// or the full schema for unstructured data
-export function extractSchema (datasetRef = {}) {
+// extractColumnHeaders should be used when trying to get the column header information
+// from the schema
+export function extractColumnHeaders (datasetRef = {}) {
var structure = datasetRef.dataset && datasetRef.dataset.structure
if (!structure) {
return undefined
}
- var format = structure.format
- if (!format) {
+ if (structure.schema && (!structure.schema.items || (structure.schema.items && !structure.schema.items.items))) {
return undefined
}
- if (format !== 'csv') {
- return structure.schema
- }
-
return structure.schema && structure.schema.items && structure.schema.items.items
}
+// extractSchema should be used when trying to extract the whole schema
+export function extractSchema (datasetRef = {}) {
+ var structure = datasetRef.dataset && datasetRef.dataset.structure
+ if (!structure) {
+ return undefined
+ }
+
+ return structure.schema
+}
export function isLocalDataset (state, path) {
const sessionID = selectSessionProfileId(state)
const datasetIds = state && state.pagination && state.pagination[usersDatasetsSection] && state.pagination[usersDatasetsSection][sessionID] && state.pagination[usersDatasetsSection][sessionID].ids
diff --git a/package.json b/package.json
index 81b50589..ddb9616e 100755
--- a/package.json
+++ b/package.json
@@ -183,7 +183,7 @@
"file-loader": "2.0.0",
"file-saver": "1.3.8",
"global": "4.3.2",
- "handsontable": "5.0.2",
+ "handsontable": "6.2.2",
"history": "4.7.2",
"html-webpack-plugin": "3.2.0",
"identity-obj-proxy": "3.0.0",
diff --git a/yarn.lock b/yarn.lock
index 27b9a186..1a5514e3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4834,9 +4834,10 @@ handlebars@^4.0.3:
optionalDependencies:
uglify-js "^3.1.4"
-handsontable@5.0.2:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-5.0.2.tgz#dcfb4341afe64c98bbbf779be40f69721e3e2c2c"
+handsontable@6.2.2:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-6.2.2.tgz#f1250f3f374abdf7d4a0080950482d3edeea8f07"
+ integrity sha512-Z/sQa51OMHH4RoeBJeANYJMJYmx5SR+/xP8JCh5mzKJnAMKoQWF1zONPNgNCFZ/LdKFmI0f34XKtU0GHW0MG/Q==
dependencies:
moment "2.20.1"
numbro "^2.0.6"