update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / admin / src / components / EditorDialog.jsx
1 import React from 'react'
2 import Modal from 'react-modal'
3 import Dialog from 'react-dialog'
4 import LeafField from './editor/LeafField'
5 import yang from '../yang/leaf-utils.js'
6 import changeCase from 'change-case'
7
8 const Button = ({ name, onClick, disabled }) => (
9     <button disabled={disabled}
10         style={{ padding: '.25rem .5rem', margin: '0 0.25rem', boxShadow: '1px 1px rgba(0, 0, 0, 0.15)' }}
11         onClick={onClick}>{name}</button>
12 );
13
14 const ButtonBar = ({ submitName, submitDisabled, onSubmit, onCancel }) => (
15     <div className='button-bar' style={{ width: '100%', textAlign: 'right', marginTop: '18px' }} >
16         <Button name="Cancel" onClick={onCancel} />
17         <Button name={submitName} onClick={onSubmit} disabled={submitDisabled} />
18     </div>
19 );
20
21 function initErrorInfo(leaves, container) {
22     return leaves.reduce((errorInfo, p) => {
23         const value = container && container[p.name];
24         const readOnly = p.isKey && !operation.isCreate;
25         if (!readOnly && p.mandatory && !value) {
26             errorInfo[p.name] = 'required';
27         }
28         return errorInfo;
29     }, {})
30 }
31
32 function buildChoiceProperty(choiceProperty) {
33     let description = choiceProperty.description + ' ';
34     const choices = choiceProperty.properties.reduce((o, p) => {
35         o[p.name] = { value: p.name };
36         description = `${description} ${p.name} - ${p.description}`;
37         return o;
38     }, {});
39     const choicePicker = Object.assign({}, choiceProperty);
40     choicePicker['type'] = 'leaf';
41     choicePicker['description'] = description;
42     choicePicker['data-type'] = {
43         enumeration: {
44             enum: choices
45         }
46     }
47     return choicePicker;
48 }
49
50 function getIntialState(props) {
51     const { isOpen, model, path, operation } = props;
52     const baseLeaves = [];
53     const choices = [];
54     const dataSet = {};
55     const errorInfo = {};
56     let shadowErrorInfo = {};
57     let leaves = baseLeaves;
58     if (isOpen && path && !operation.isDelete) {
59         const element = model.getElement(path);
60         const dataDivided = {
61             leaf: baseLeaves,
62             leaf_list: baseLeaves,
63             choice: choices,
64         };
65         element.schema.properties.forEach((property, index) => {
66             const list = dataDivided[property.type]
67             list && list.push(property)
68         });
69         if (!operation.isCreate) {
70             // we are not going to prompt for choices so add in appropriate files now
71             choices.forEach((choice) => {
72                 const caseSelected = choice.properties.find(c => c.properties && c.properties.some(p => element.value[p.name]));
73                 if (caseSelected) {
74                     caseSelected.properties.forEach(p => yang.isLeafOrLeafList(p) && baseLeaves.push(p));
75                 }
76             });
77         } else {
78             // on create we first propmt for "features"
79             leaves = choices.map(choice => buildChoiceProperty(choice));
80         }
81         shadowErrorInfo = initErrorInfo(leaves, element.value);
82     }
83     return { dataSet, shadowErrorInfo, errorInfo, baseLeaves, leaves, choices };
84 }
85
86 export default class extends React.Component {
87     constructor(props) {
88         super(props);
89         this.state = getIntialState(props);
90         this.state.showHelp = true;
91     }
92
93     handleCloseEditor = () => {
94     };
95
96     componentWillReceiveProps(nextProps) {
97         if (this.props.isOpen !== nextProps.isOpen) {
98             this.setState(getIntialState(nextProps));
99         }
100     }
101
102     render() {
103         try {
104             const { isOpen, model, isReadonly, properties, operation, onSave, onCancel } = this.props;
105             if (!isOpen) {
106                 return null;
107             }
108             let dataPath = this.props.path.slice();
109             const element = model.getElement(dataPath);
110             const container = element.value;
111             let editors = null;
112             let submitHandler = () => {
113                 if (Object.keys(this.state.errorInfo).length === 0) {
114                     onSave(this.state.dataSet);
115                 }
116             }
117             const checkForSubmitKey = (e) => {
118                 if (e.keyCode == 13) {
119                     e.preventDefault();
120                     e.stopPropagation();
121                     submitHandler();
122                 }
123             };
124             let submitButtonLabel = operation.isCreate ? "Add" : "Save";
125             let submitDisabled = true;
126             let headerText = null;
127             if (operation.isDelete) {
128                 const id = dataPath[dataPath.length - 1];
129                 const deletePrompt = `Delete ${id}?`;
130                 submitButtonLabel = "Delete";
131                 submitDisabled = false;
132                 editors = (<div style={{ paddingBottom: '10px' }}>{deletePrompt}</div>);
133             } else {
134                 let { leaves, choices } = this.state;
135                 if (choices.length) {
136                     if (operation.isCreate) {
137                         headerText = `Select feature(s) for ${element.name}`
138                         submitButtonLabel = "Continue";
139                         submitHandler = () => {
140                             const { dataSet, baseLeaves } = this.state;
141                             const leaves = choices.reduce((leafList, choice) => {
142                                 const caseSelected = dataSet[choice.name].value;
143                                 delete dataSet[choice.name];
144                                 if (caseSelected) {
145                                     return leafList.concat(choice.properties.find((p) => p.name === caseSelected).properties.reduce((list, p) => {
146                                         yang.isLeafOrLeafList(p) && list.push(p);
147                                         return list;
148                                     }, []));
149                                 }
150                                 return leafList;
151                             }, baseLeaves.slice());
152                             const shadowErrorInfo = initErrorInfo(leaves, element.value);
153                             this.setState({ dataSet, leaves, choices: [], errorInfo: {}, shadowErrorInfo });
154                         }
155                     }
156                 }
157                 submitDisabled = (Object.keys(this.state.shadowErrorInfo).length
158                     || (!operation.isCreate && Object.keys(this.state.dataSet).length === 0));
159                 // process the named field value change
160                 function processFieldValueChange(property, currentValue, value) {
161                     console.debug(`processed change for -- ${name} -- with value -- ${value}`);
162                     const dataSet = this.state.dataSet;
163                     const name = property.name;
164                     if ((currentValue && currentValue !== value) || (!currentValue && value)) {
165                         dataSet[name] = { property, value, currentValue };
166                     } else {
167                         delete dataSet[name];
168                     }
169                     const { errorInfo, shadowErrorInfo } = this.state;
170                     delete errorInfo[name];
171                     delete shadowErrorInfo[name];
172                     this.setState({ dataSet, errorInfo, shadowErrorInfo });
173                 }
174
175                 function onErrorHandler(name, message) {
176                     const { errorInfo, shadowErrorInfo } = this.state;
177                     errorInfo[name] = message;
178                     shadowErrorInfo[name] = message;
179                     this.setState({ errorInfo, shadowErrorInfo });
180                 }
181
182                 editors = leaves.reduce((editors, property, index) => {
183                     const itemPath = dataPath.slice();
184                     itemPath.push(property.name);
185                     const props = { model, 'path': itemPath };
186                     const value = container && container[property.name];
187                     let readOnly = isReadonly;
188                     let extraHelp = null;
189                     if (!isReadonly) {
190                         if (yang.isKey(property) && !operation.isCreate) {
191                             extraHelp = "Id fields are not modifiable.";
192                             readOnly = true;
193                         } else if (yang.isLeafList(property)) {
194                             extraHelp = "Enter a comma separated list of values."
195                         }
196                     }
197                     editors.push(
198                         <LeafField
199                             key={property.name}
200                             container={container}
201                             property={property}
202                             path={dataPath}
203                             value={value}
204                             showHelp={this.state.showHelp}
205                             onChange={processFieldValueChange.bind(this, property, value)}
206                             onError={onErrorHandler.bind(this, property.name)}
207                             readOnly={readOnly}
208                             extraHelp={extraHelp}
209                             errorMessage={this.state.errorInfo[property.name]}
210                         />
211                     );
212                     return editors;
213                 }, [])
214             }
215             const customStyles = {
216                 content: {
217                     top: '50%',
218                     left: '50%',
219                     right: 'auto',
220                     bottom: 'auto',
221                     marginRight: '-50%',
222                     transform: 'translate(-50%, -50%)'
223                 }
224             };
225             const dlgHeader = headerText ? (<div style={{ paddingBottom: '16px' }} >{headerText}</div>) : null;
226             return (
227                 <Modal
228                     isOpen={isOpen}
229                     contentLabel="Edit"
230                     onRequestClose={onCancel}
231                     shouldCloseOnOverlayClick={false}
232                     style={customStyles}
233                 >
234                     <div className='leaf-group' style={{ maxWidth: '400px', maxHeight: '600px' }} onKeyUp={checkForSubmitKey} >
235                         {dlgHeader}
236                         {editors}
237                         <ButtonBar
238                             submitName={submitButtonLabel}
239                             submitDisabled={submitDisabled}
240                             onSubmit={submitHandler}
241                             onCancel={onCancel} />
242                     </div>
243                 </Modal >
244             )
245         } catch (e) {
246             console.error("component render", e);
247         }
248     }
249 }