Skip to content
This repository has been archived by the owner on Jul 3, 2023. It is now read-only.

[WIP] Configuration View #19

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9cc5f06
Initial work on Configuration
JosephusPaye May 18, 2017
1f61d48
Convert indents to 2-spaces
JosephusPaye May 19, 2017
a097af5
Add trailing newline to SVG files, use 2 space indents
JosephusPaye May 19, 2017
ead72c5
Remove trailing commas from interfaces
JosephusPaye May 19, 2017
5826601
Remove default exports
JosephusPaye May 19, 2017
6fb0b90
Merge branch 'master' into feature/configuration
JosephusPaye May 22, 2017
b101805
Remove unnecessary tree constructor, render children only when expanded
JosephusPaye May 24, 2017
5188eac
Rename tree_store to tree_model
JosephusPaye May 24, 2017
3e28be4
Extract sidebar wrapper to standalone panel component
JosephusPaye May 24, 2017
e01ab49
Extract view wrapper (and header) to dedicated view component
JosephusPaye May 24, 2017
ab6b8a7
Add controller and model, move sample data to separate file
JosephusPaye May 24, 2017
6e431ac
Use ul/li tags for tree
JosephusPaye May 24, 2017
833fe7a
Add comment about inline paddingLeft
JosephusPaye May 24, 2017
9d6f6a4
Tweak indentation comment
JosephusPaye May 24, 2017
76087a8
Merge origin/master
JosephusPaye Jun 20, 2017
a82d104
Merge branch 'master' into feature/configuration
JosephusPaye Jun 22, 2017
01b483b
Merge branch 'master' into feature/configuration
JosephusPaye Jun 23, 2017
6477f18
Merge branch 'master' into feature/configuration
JosephusPaye Jun 27, 2017
9135187
WIP configuration editor
JosephusPaye Jun 27, 2017
8ef925b
Make the basic editor work, fix styling
JosephusPaye Jun 30, 2017
27f8cff
Merge branch 'master' into feature/configuration
JosephusPaye Jul 14, 2017
fc52617
.
JosephusPaye Jul 26, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/client/components/app/style.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,500');

* {
box-sizing: border-box;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this will ever come back to bite us? Would libraries ever assume content-box is the default and break? Maybe its fine and we'll revisit if its a problem.

Copy link
Member Author

@JosephusPaye JosephusPaye May 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is this, which allows for easily resetting the box-sizing locally, when needed:

html {
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

Reference: https://css-tricks.com/box-sizing/#article-header-id-6

}

html,
body {
margin: 0;
Expand Down
74 changes: 74 additions & 0 deletions src/client/components/configuration/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { action, observable } from 'mobx'
import { ConfigurationModel } from './model'
import { TreeNode } from './model'
import { ConfigurationField } from './model'

export class ConfigurationController {
private model: ConfigurationModel

constructor(opts: { model: ConfigurationModel }) {
Object.assign(this, opts)

console.log(this.model)
}

public static of(opts: { model: ConfigurationModel }) {
return new ConfigurationController(opts)
}

@action
public updateSaveOnChange = (saveOnChange: boolean): void => {
this.model.saveOnChange = saveOnChange
}

@action
public onNodeClick = (node: TreeNode): void => {
if (node.leaf) {
this.selectNode(node)
} else {
this.toggleNodeExpansion(node)
}
}

@action
public selectNode = (node: TreeNode) => {
if (this.model.selectedFile) {
this.model.selectedFile.selected = false

if (!this.model.selectedFile.status.changed) {
this.model.selectedFile.status.lastRevision = null
}
}

node.selected = true
node.status.lastRevision = JSON.stringify(node.data)

this.model.selectedFile = node
}

@action
public toggleNodeExpansion = (node: TreeNode) => {
node.expanded = !node.expanded
}

@action
public onEditorChange = (field: ConfigurationField, newValue: any, e: any) => {
field.value = newValue

if (this.model.selectedFile !== null) {
this.model.selectedFile.status.changed = true

const selectedFilePath = this.model.selectedFile.data!.path

if (!this.model.changedFields[selectedFilePath]) {
this.model.changedFields[selectedFilePath] = {}
}

this.model.changedFields[selectedFilePath][field.path!] = field
}

// TODO (Paye):
// If in live (auto save) mode, send new value of field to robot, else
// save it in the list of changes and send in all changes when the user clicks "Save"
}
}
87 changes: 87 additions & 0 deletions src/client/components/configuration/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { TreeNode } from './model'
import * as files from './sample-config-files.json'

interface File {
path: string
content: any
}

export function createTreeFromFiles(files: any): TreeNode {
// The tree (root node)
const tree: TreeNode = {
label: 'config/',
expanded: true,
leaf: false,
selected: false,
children: [],
status: {
changed: false,
lastRevision: null
}
}

// A map of file paths to tree nodes
const pathNodeMap = {
'config/': tree,
}

// Create a node for each file and add it to the tree
files.forEach((file: File) => {
// Remove consecutive slashes from the path
file.path = file.path.replace(/\/\//g, '/')

// Remove the first segment ('config/') from the path and pass it as parentPath,
// since we've already created it as the root node - this assumes that every
// config file is in one directory - config/
createTreeNode(file.path.split('/').slice(1), 'config/', file, pathNodeMap)
})

return tree
}

export function createTreeNode(pathSegments: string[], parentPath: string, file: File, pathNodeMap: any) {
const isFolder = pathSegments.length > 1
const label = pathSegments[0] + (isFolder ? '/' : '')
const currentPath = parentPath + label

// Abort if the current path is a file node already in the tree
if (!isFolder && pathNodeMap[currentPath] !== undefined) {
return
}

// Recurse and abort if the current path is a folder node already in the tree
if (isFolder && pathNodeMap[currentPath] !== undefined) {
createTreeNode(pathSegments.slice(1), currentPath, file, pathNodeMap)
return
}

// Create the new node
const node: TreeNode = {
label,
expanded: false,
leaf: !isFolder,
selected: false,
data: isFolder ? undefined : file,
children: [],
status: {
changed: false,
lastRevision: null
}
}

// Add the new node to the tree
pathNodeMap[parentPath].children.push(node)
// pathNodeMap[parentPath].children.sort((a: TreeNode, b: TreeNode) => {
// return (a.leaf === b.leaf ? a.label.toLowerCase() > b.label.toLowerCase() : a.leaf > b.leaf)
// })

// Add the new node to the path map
pathNodeMap[currentPath] = node

// Recurse if the new node is a folder
if (isFolder) {
createTreeNode(pathSegments.slice(1), currentPath, file, pathNodeMap)
}
}

export const configurationData = createTreeFromFiles(files)
21 changes: 21 additions & 0 deletions src/client/components/configuration/editor/editor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.editor {
padding: 18px 0;
}

.editor,
.editor * {
font-family: monospace;
}

.editorLine {
margin-bottom: 4px;
min-height: 24px;
display: flex;
align-items: center;
/*border-bottom: 1px solid #EEE;*/
}

.editorLine__nestedList {
margin-bottom: 0;
/*border-bottom: 0;*/
}
37 changes: 37 additions & 0 deletions src/client/components/configuration/editor/editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { observer } from 'mobx-react'
import * as React from 'react'
import * as style from './editor.css'

import { ListField } from './fields/list_field'
import { MapField } from './fields/map_field'
import { ScalarField } from './fields/scalar_field'
import { ConfigurationField, ConfigurationFile } from '../model'

export interface FieldProps {
data: ConfigurationField,
onChange?(field: ConfigurationField, newValue: any, e: any): void
}

export interface EditorProps {
data: ConfigurationFile,
onChange?(field: ConfigurationField, newValue: any, e: any): void
}

@observer
export class Editor extends React.Component<EditorProps> {
public render(): JSX.Element {
const field = this.props.data.content
return (
<div className={style.editor}>
{ field.type === 'MAP_VALUE' && <MapField data={field} onChange={this.props.onChange} /> }
{ field.type === 'LIST_VALUE' && <ListField data={field} onChange={this.props.onChange} /> }
{ (field.type === 'BOOL_VALUE' ||
field.type === 'INT_VALUE' ||
field.type === 'DOUBLE_VALUE' ||
field.type === 'STRING_VALUE') &&
<ScalarField data={field} onChange={this.props.onChange} />
}
</div>
)
}
}
28 changes: 28 additions & 0 deletions src/client/components/configuration/editor/fields/list_field.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.listField {
padding: 0;
list-style: none;
margin: 0;
width: 100%;
}

.listField__header {
color: #63a35c;
cursor: default;
}

.listField__header::after {
display: inline-block;
content: ':';
}

.listField__subFields {
padding-left: 22px;
}

.listField__subFields .listField__subField::before {
display: inline-block;
content: '-';
margin-right: 8px;
color: rgba(0,0,0,0.5);
align-self: flex-start;
}
54 changes: 54 additions & 0 deletions src/client/components/configuration/editor/fields/list_field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { observer } from 'mobx-react'
import * as React from 'react'
import * as style from './list_field.css'
import * as editorStyle from '../editor.css'
import * as classnames from 'classnames'

import { ConfigurationField } from '../../model'
import { FieldProps } from '../editor'
import { MapField } from './map_field'
import { NumberValue } from '../values/number_value'
import { StringValue } from '../values/string_value'

@observer
export class ListField extends React.Component<FieldProps> {
public render(): JSX.Element {
return (
<ul className={style.listField}>
{ this.props.data.name &&
<div className={style.listField__header + ' ' + editorStyle.editorLine}>{ this.props.data.name }</div>
}

<div className={style.listField__subFields}>
{ this.props.data.value.map((field: ConfigurationField) => {
switch (field.type) {
case 'INT_VALUE':
case 'DOUBLE_VALUE':
return <li className={style.listField__subField + ' ' + editorStyle.editorLine} key={field.uid}>
<NumberValue data={field} onChange={this.props.onChange} />
</li>
case 'STRING_VALUE':
return <li className={style.listField__subField + ' ' + editorStyle.editorLine} key={field.uid}>
<StringValue data={field} onChange={this.props.onChange} />
</li>
case 'MAP_VALUE':
return <li className={style.listField__subField + ' ' + editorStyle.editorLine} key={field.uid}>
<MapField data={field} onChange={this.props.onChange} />
</li>
case 'LIST_VALUE':
const classes = classnames(
style.listField__subField, editorStyle.editorLine, editorStyle.editorLine__nestedList
)
return <li className={classes} key={field.uid}>
<ListField data={field} onChange={this.props.onChange} />
</li>
default:
// do nothing
}
})
}
</div>
</ul>
)
}
}
17 changes: 17 additions & 0 deletions src/client/components/configuration/editor/fields/map_field.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.mapField {

}

.mapField__header {
color: #63a35c;
cursor: default;
}

.mapField__header::after {
display: inline-block;
content: ':';
}

.mapField__subFields {
padding-left: 22px;
}
43 changes: 43 additions & 0 deletions src/client/components/configuration/editor/fields/map_field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { observer } from 'mobx-react'
import * as React from 'react'
import * as style from './map_field.css'
import * as editorStyle from '../editor.css'

import { ConfigurationField } from '../../model'
import { FieldProps } from '../editor'
import { ListField } from './list_field'
import { ScalarField } from './scalar_field'

@observer
export class MapField extends React.Component<FieldProps> {
public render(): JSX.Element {
return (
<div className={style.mapField}>
{ this.props.data.name &&
<div className={style.mapField__header + ' ' + editorStyle.editorLine}>{ this.props.data.name }</div>
}

<div className={style.mapField__subFields}>
{ Object.keys(this.props.data.value).map(fieldName => {
const field: ConfigurationField = this.props.data.value[fieldName]

switch (field.type) {
case 'MAP_VALUE':
return <MapField data={field} onChange={this.props.onChange} key={field.uid} />
case 'LIST_VALUE':
return <ListField data={field} onChange={this.props.onChange} key={field.uid} />
case 'BOOL_VALUE':
case 'INT_VALUE':
case 'DOUBLE_VALUE':
case 'STRING_VALUE':
return <ScalarField data={field} onChange={this.props.onChange} key={field.uid} />
default:
// do nothing
}
})
}
</div>
</div>
)
}
}
Loading