update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / admin / src / components / ModelExplorer.jsx
1 import React from 'react'
2 import ModalDialog from 'react-modal'
3 import _set from 'lodash/set';
4 import _get from 'lodash/get';
5 import ContainerColumn from './ContainerColumn'
6 import ChoiceColumn from './ChoiceColumn'
7 import ListColumn from './ListColumn'
8 import LoadingColumn from './LoadingColumn'
9 import ListEntryColumn from './ListEntryColumn'
10 import LoadingCard from './LoadingCard'
11 import ListCard from './ListCard'
12 import ContainerCard from './ContainerCard'
13 import EditorDialog from './EditorDialog'
14
15 function findItemInList(list, key, keyValue) {
16     const keyPath = Array.isArray(keyValue) ? keyValue : key.length > 1 ? JSON.parse(keyValue) : [keyValue];
17     if (key.length > 1) {
18         return list.find(item => {
19             return key.every((k, i) => item[k] === keyPath[i]);
20         });
21     } else {
22         const leaf = key[0];
23         const match = keyPath[0];
24         return list.find(item => {
25             return item[leaf] === match;
26         });
27     }
28 }
29 function getItemInfoFunction(schema) {
30     return function (item) {
31         return ModelExplorer.getItemInfo(schema.key, item);
32     }
33 }
34
35 function makeNameFromKeyPath(key, path, name) {
36     return path.length > 1 ? `${path[0]} (${path.slice(1).join(" ,")})` : name || path[0];
37 }
38
39 class ExplorerModel {
40     constructor(dataModel) {
41         this.dataModel = dataModel;
42         this.topNode = dataModel.path.split('/').pop();
43     }
44     getElement(path) {
45         const dataModel = this.dataModel;
46         if (dataModel.isLoading) {
47             return null;
48         }
49         if (dataModel.updatingPath && dataModel.updatingPath.every((p, i) => path[i] === p)) {
50             return { type: 'loading' }
51         }
52         return path.reduce((parent, node, index) => {
53             const element = Object.assign({}, parent);
54             element.path.push(node);
55             if (parent.type === 'list') {
56                 element.type = 'list-entry'
57                 element.value = findItemInList(parent.value, parent.schema.key, node);
58                 element.keyValue = parent.schema.key.map(leaf => element.value[leaf]);
59                 element.name = makeNameFromKeyPath(parent.schema.key, element.keyValue, element.value['name']);
60                 element.getItemInfo = getItemInfoFunction(parent.schema);
61             } else {
62                 element.schema = parent.schema.properties.find(property => property.name === node)
63                 element.type = element.schema.type;
64                 element.value = element.type === 'choice' ? parent.value : parent.value && parent.value[node];
65                 element.name = node.split(':').pop();
66                 if (element.type === 'list') {
67                     element.getItemInfo = getItemInfoFunction(element.schema);
68                 }
69             }
70             return element;
71         }, {
72                 schema: dataModel.schema[this.topNode],
73                 getItemInfo: getItemInfoFunction(this.schema),
74                 value: dataModel.data,
75                 type: dataModel.schema[this.topNode].type,
76                 path: dataModel.path.split('/')
77             }
78         )
79     }
80 }
81
82 const columnComponent = {
83     'list': ListColumn,
84     'container': ContainerColumn,
85     'list-entry': ListEntryColumn,
86     'choice': ChoiceColumn,
87     'loading': LoadingColumn
88 }
89
90 class ModelExplorer extends React.Component {
91     static getExplorerModel(dataModel) {
92         return new ExplorerModel(dataModel);
93     }
94     static getItemInfo(keyDef, item) {
95         const path = keyDef.map(leaf => item[leaf]);
96         const name = makeNameFromKeyPath(keyDef, path, item.name);
97         return {
98             path,
99             name
100         };
101     }
102
103     constructor(props) {
104         super(props);
105         const columns = props.columns || [['/']];
106         this.state = { columns };
107     }
108
109     componentWillReceiveProps(nextProps) {
110         if (!this.state.columns && nextProps.model) {
111             this.setState({ columns: [[Array.isArray(nextProps.model) ? '' : '/']] })
112         }
113     }
114
115     render() {
116         const { model, onUpdate } = this.props;
117         let { columns, isEditMode, editPath, editOperation } = this.state;
118
119         const openElement = (col, path) => {
120             columns = columns.slice(0, col + 1);
121             columns.push(path);
122             this.setState({ columns })
123         }
124
125         const closeLastColumn = () => {
126             columns = columns.slice();
127             columns.pop();
128             this.setState({ columns })
129         }
130
131         const lastCol = columns.length - 1;
132         const modelColumns = columns && columns.map((path, col) => {
133             const open = openElement.bind(this, col);
134             const props = {
135                 key: path.join('/'),
136                 model,
137                 path,
138                 selected: col < lastCol ? columns[col + 1][path.length] : null,
139                 isLast: col === lastCol,
140                 openElement: openElement.bind(this, col),
141                 columnCloser: col === lastCol && col && closeLastColumn,
142                 editElement: (path, op) => this.setState({
143                     isEditMode: true,
144                     editOperation: op || 'update',
145                     editPath: path
146                 })
147             }
148             return React.createElement(columnComponent[model.getElement(path).type], props)
149         })
150
151         const updateModel = (data) => {
152             let { columns, isEditMode, editPath, editOperation } = this.state;
153             onUpdate(editPath, editOperation, data) && columns.pop();
154             this.setState({
155                 columns,
156                 isEditMode: false,
157                 editOperation: null,
158                 editPath: null
159             });
160         }
161
162         return (
163             <div className='model-explorer'>
164                 <div style={{ width: '100%', display: 'flex', flexDirection: 'row', overflow: 'scroll' }}>
165                     {modelColumns}
166                 </div>
167                 <EditorDialog
168                     isOpen={isEditMode}
169                     operation={{
170                         isCreate: editOperation === 'create',
171                         isDelete: editOperation === 'delete'}}
172                     model={model}
173                     path={editPath}
174                     onCancel={() => this.setState({ isEditMode: false })}
175                     onSave={updateModel} />
176             </div>
177         )
178     }
179 }
180
181 export default ModelExplorer