blob: a7e36a39fca023aa47288abb77297fa1f2ada674 [file] [log] [blame]
talig8e9c0652017-12-20 14:30:43 +02001/*!
2 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 * or implied. See the License for the specific language governing
14 * permissions and limitations under the License.
15 */
16import React from 'react';
17import i18n from 'nfvo-utils/i18n/i18n.js';
18import union from 'lodash/union.js';
19import Button from 'sdc-ui/lib/react/Button.js';
20// import Checkbox from 'sdc-ui/lib/react/Checkbox.js';
21import Input from 'nfvo-components/input/validation/Input.jsx';
22import GridSection from 'nfvo-components/grid/GridSection.jsx';
23import GridItem from 'nfvo-components/grid/GridItem.jsx';
24import SVGIcon from 'sdc-ui/lib/react/SVGIcon.js';
25import Radio from 'sdc-ui/lib/react/Radio.js';
26import equal from 'deep-equal';
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020027import { ResolutionTypes } from './MergeEditorConstants.js';
talig8e9c0652017-12-20 14:30:43 +020028
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020029class ConflictCategory extends React.Component {
30 state = {
31 resolution: ResolutionTypes.YOURS
32 };
talig8e9c0652017-12-20 14:30:43 +020033
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020034 getTitle(conflictType, conflictName) {
35 if (
36 typeof conflictName === 'undefined' ||
37 conflictType === conflictName
38 ) {
39 return i18n(conflictType);
40 } else {
41 return `${i18n(conflictType)}: ${conflictName}`;
42 }
43 }
talig8e9c0652017-12-20 14:30:43 +020044
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020045 render() {
46 let {
47 collapseExpand,
48 conflict: { id: conflictId, type, name },
49 isCollapsed,
50 item: { id: itemId, version },
51 onResolveConflict
52 } = this.props;
53 let { resolution } = this.state;
54 const iconClass = isCollapsed ? 'merge-chevron' : 'merge-chevron right';
talig8e9c0652017-12-20 14:30:43 +020055
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020056 return (
57 <div key={'conflictCategory_' + conflictId}>
58 <GridSection className="conflict-section">
59 <GridItem>
60 <div
61 className="collapsible-section"
62 onClick={collapseExpand}>
63 <SVGIcon
64 name={isCollapsed ? 'chevronDown' : 'chevronUp'}
65 iconClassName={iconClass}
66 />
67 <div className="conflict-title">
68 {this.getTitle(type, name)}
69 </div>
70 </div>
71 </GridItem>
72 <GridItem className="yours">
73 <Radio
74 name={'radio_' + conflictId}
75 checked={resolution === ResolutionTypes.YOURS}
76 value="yours"
77 onChange={() =>
78 this.setState({
79 resolution: ResolutionTypes.YOURS
80 })
81 }
82 data-test-id={'radio_' + conflictId + '_yours'}
83 />
84 </GridItem>
85 <GridItem className="theirs">
86 <Radio
87 name={'radio_' + conflictId}
88 checked={resolution === ResolutionTypes.THEIRS}
89 value="theirs"
90 onChange={() =>
91 this.setState({
92 resolution: ResolutionTypes.THEIRS
93 })
94 }
95 data-test-id={'radio_' + conflictId + '_theirs'}
96 />
97 </GridItem>
98 <GridItem className="resolve">
99 <Button
100 className="conflict-resolve-btn"
101 btnType="outline"
102 color="gray"
103 onClick={() =>
104 onResolveConflict({
105 conflictId,
106 resolution,
107 itemId,
108 version
109 })
110 }>
111 {i18n('Resolve')}
112 </Button>
113 </GridItem>
114 </GridSection>
115 <div>{isCollapsed && this.props.children}</div>
116 </div>
117 );
118 }
119}
talig8e9c0652017-12-20 14:30:43 +0200120
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200121class TextCompare extends React.Component {
122 render() {
123 // let rand = Math.random() * (3000 - 1) + 1;
124 let {
125 yours,
126 theirs,
127 field,
128 type,
129 isObjName,
130 conflictsOnly
131 } = this.props;
132 let typeYours = typeof yours;
133 let typeTheirs = typeof theirs;
talig8e9c0652017-12-20 14:30:43 +0200134
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200135 let parsedType = `${type}/${field}`.replace(/\/[0-9]+/g, '/index');
136 let level = type.split('/').length;
talig8e9c0652017-12-20 14:30:43 +0200137
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200138 if (typeYours === 'boolean' || typeTheirs === 'boolean') {
139 yours = yours ? i18n('Yes') : i18n('No');
140 theirs = theirs ? i18n('Yes') : i18n('No');
141 }
talig8e9c0652017-12-20 14:30:43 +0200142
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200143 /*if ((typeYours !== 'string' && typeYours !== 'undefined') || (typeTheirs !== 'string' && typeTheirs !== 'undefined')) {
talig8e9c0652017-12-20 14:30:43 +0200144 return (<div className='merge-editor-text-field field-error'>{field} cannot be parsed for display</div>);
145 }*/
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200146 let isDiff = yours !== theirs;
147 if (
148 !isObjName &&
149 ((!isDiff && conflictsOnly) ||
150 (yours === '' && theirs === '') ||
151 (typeYours === 'undefined' && typeTheirs === 'undefined'))
152 ) {
153 return null;
154 }
talig8e9c0652017-12-20 14:30:43 +0200155
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200156 return (
157 <GridSection
158 className={
159 isDiff
160 ? 'merge-editor-text-field diff'
161 : 'merge-editor-text-field'
162 }>
163 <GridItem className="field-col grid-col-title" stretch>
164 <div
165 className={`field ${
166 isDiff ? 'diff' : ''
167 } field-name level-${level} ${
168 isObjName ? 'field-object-name' : ''
169 }`}>
170 {i18n(parsedType)}
171 </div>
172 </GridItem>
173 <GridItem className="field-col grid-col-yours" stretch>
174 <div
175 className={`field field-yours ${
176 !yours ? 'empty-field' : ''
177 }`}>
178 {yours || (isObjName ? '' : '━━')}
179 </div>
180 </GridItem>
181 <GridItem className="field-col grid-col-theirs" stretch>
182 <div
183 className={`field field-theirs ${
184 !theirs ? 'empty-field' : ''
185 }`}>
186 {theirs || (isObjName ? '' : '━━')}
187 </div>
188 </GridItem>
189 <GridItem stretch />
190 </GridSection>
191 );
192 }
193}
talig8e9c0652017-12-20 14:30:43 +0200194
195class MergeEditorView extends React.Component {
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200196 state = {
197 collapsingSections: {},
198 conflictsOnly: false
199 };
talig8e9c0652017-12-20 14:30:43 +0200200
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200201 render() {
202 let {
203 conflicts,
204 item,
205 conflictFiles,
206 onResolveConflict,
207 currentScreen,
208 resolution
209 } = this.props;
talig8e9c0652017-12-20 14:30:43 +0200210
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200211 return (
212 <div className="merge-editor">
213 {conflictFiles && this.renderConflictTableTitles()}
214 <div className="merge-editor-body">
215 {conflictFiles &&
216 conflictFiles
217 .sort((a, b) => a.type > b.type)
218 .map(file => (
219 <ConflictCategory
220 key={'conflict_' + file.id}
221 conflict={file}
222 item={item}
223 isCollapsed={
224 this.state.collapsingSections[file.id]
225 }
226 collapseExpand={() => {
227 this.updateCollapseState(file.id);
228 }}
229 onResolveConflict={cDetails =>
230 onResolveConflict({
231 ...cDetails,
232 currentScreen
233 })
234 }>
235 {conflicts &&
236 conflicts[file.id] &&
237 this.getUnion(
238 conflicts[file.id].yours,
239 conflicts[file.id].theirs
240 ).map(field => {
241 return this.renderField(
242 field,
243 file,
244 conflicts[file.id].yours[field],
245 conflicts[file.id].theirs[
246 field
247 ],
248 resolution
249 );
250 })}
251 </ConflictCategory>
252 ))}
253 </div>
254 </div>
255 );
256 }
talig8e9c0652017-12-20 14:30:43 +0200257
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200258 renderConflictTableTitles() {
259 return (
260 <GridSection className="conflict-titles-section">
261 <GridItem>{i18n('Page')}</GridItem>
262 <GridItem className="yours">{i18n('Local (Me)')}</GridItem>
263 <GridItem className="theirs">{i18n('Last Committed')}</GridItem>
264 <GridItem className="resolve">
265 <Input
266 label={i18n('Show Conflicts Only')}
267 type="checkbox"
268 value={this.state.conflictsOnly}
269 onChange={e => this.setState({ conflictsOnly: e })}
270 />
271 </GridItem>
272 </GridSection>
273 );
274 }
275 // <Checkbox
276 // label={i18n('Show Conflicts Only')}
277 // value={this.state.conflictsOnly}
278 // checked={this.state.conflictsOnly}
279 // onChange={checked => this.setState({conflictsOnly: checked})} />
talig8e9c0652017-12-20 14:30:43 +0200280
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200281 renderObjects(yours, theirs, fileType, field, id, resolution) {
282 if (equal(yours, theirs)) {
283 return;
284 }
285 let { conflictsOnly } = this.state;
286 return (
287 <div key={`obj_${fileType}/${field}_${id}`}>
288 <TextCompare
289 field={field}
290 type={fileType}
291 conflictsOnly={conflictsOnly}
292 yours=""
293 theirs=""
294 isObjName
295 resolution={resolution}
296 />
297 <div className="field-objects">
298 <div>
299 {this.getUnion(yours, theirs).map(key =>
300 this.renderField(
301 key,
302 { type: `${fileType}/${field}`, id },
303 yours && yours[key],
304 theirs && theirs[key]
305 )
306 )}
307 </div>
308 </div>
309 </div>
310 );
311 }
talig8e9c0652017-12-20 14:30:43 +0200312
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200313 renderList(yours = [], theirs = [], type, field, id, resolution) {
314 let theirsList = theirs.join(', ');
315 let yoursList = yours.join(', ');
316 let { conflictsOnly } = this.state;
317 return (
318 <TextCompare
319 key={'text_' + id + '_' + field}
320 field={field}
321 type={type}
322 yours={yoursList}
323 theirs={theirsList}
324 conflictsOnly={conflictsOnly}
325 resolution={resolution}
326 />
327 );
328 }
talig8e9c0652017-12-20 14:30:43 +0200329
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200330 renderField(field, file, yours, theirs, resolution) {
331 if (yours) {
332 if (Array.isArray(yours)) {
333 return this.renderList(
334 yours,
335 theirs,
336 file.type,
337 field,
338 file.id,
339 resolution
340 );
341 } else if (typeof yours === 'object') {
342 return this.renderObjects(
343 yours,
344 theirs,
345 file.type,
346 field,
347 file.id,
348 resolution
349 );
350 }
351 } else if (theirs) {
352 if (Array.isArray(theirs)) {
353 return this.renderList(
354 yours,
355 theirs,
356 file.type,
357 field,
358 file.id,
359 resolution
360 );
361 } else if (typeof theirs === 'object') {
362 return this.renderObjects(
363 yours,
364 theirs,
365 file.type,
366 field,
367 file.id,
368 resolution
369 );
370 }
371 }
372 let { conflictsOnly } = this.state;
373 return (
374 <TextCompare
375 key={'text_' + file.id + '_' + field}
376 resolution={resolution}
377 field={field}
378 type={file.type}
379 yours={yours}
380 theirs={theirs}
381 conflictsOnly={conflictsOnly}
382 />
383 );
384 }
talig8e9c0652017-12-20 14:30:43 +0200385
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200386 getUnion(yours = {}, theirs = {}) {
387 let yoursKeys = Object.keys(yours);
388 let theirsKeys = Object.keys(theirs);
389 let myUn = union(yoursKeys, theirsKeys);
390 return myUn; //.sort((a, b) => a > b);
391 }
talig8e9c0652017-12-20 14:30:43 +0200392
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200393 updateCollapseState(conflictId) {
394 const {
395 fetchConflict,
396 item: { id: itemId, version } /*conflicts*/
397 } = this.props;
398 let isCollapsed = this.state.collapsingSections[conflictId];
399 // if (!isCollapsed && !(conflicts && conflictId in conflicts)) {
400 if (!isCollapsed) {
401 fetchConflict({ cid: conflictId, itemId, version });
402 }
403 this.setState({
404 collapsingSections: {
405 ...this.state.collapsingSections,
406 [conflictId]: !isCollapsed
407 }
408 });
409 }
talig8e9c0652017-12-20 14:30:43 +0200410}
411
412export default MergeEditorView;