blob: 9abe5e12ce4a1061ea9eea45f7f7ea1c7b9bddff [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';
Arielka51eac02019-07-07 12:56:11 +030019import { Button, SVGIcon, Radio } from 'onap-ui-react';
20// import Checkbox from 'onap-ui-react/Checkbox.js';
talig8e9c0652017-12-20 14:30:43 +020021import Input from 'nfvo-components/input/validation/Input.jsx';
22import GridSection from 'nfvo-components/grid/GridSection.jsx';
23import GridItem from 'nfvo-components/grid/GridItem.jsx';
talig8e9c0652017-12-20 14:30:43 +020024import equal from 'deep-equal';
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020025import { ResolutionTypes } from './MergeEditorConstants.js';
talig8e9c0652017-12-20 14:30:43 +020026
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020027class ConflictCategory extends React.Component {
28 state = {
29 resolution: ResolutionTypes.YOURS
30 };
talig8e9c0652017-12-20 14:30:43 +020031
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020032 getTitle(conflictType, conflictName) {
33 if (
34 typeof conflictName === 'undefined' ||
35 conflictType === conflictName
36 ) {
37 return i18n(conflictType);
38 } else {
39 return `${i18n(conflictType)}: ${conflictName}`;
40 }
41 }
talig8e9c0652017-12-20 14:30:43 +020042
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020043 render() {
44 let {
45 collapseExpand,
46 conflict: { id: conflictId, type, name },
47 isCollapsed,
48 item: { id: itemId, version },
49 onResolveConflict
50 } = this.props;
51 let { resolution } = this.state;
52 const iconClass = isCollapsed ? 'merge-chevron' : 'merge-chevron right';
talig8e9c0652017-12-20 14:30:43 +020053
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020054 return (
55 <div key={'conflictCategory_' + conflictId}>
56 <GridSection className="conflict-section">
57 <GridItem>
58 <div
59 className="collapsible-section"
60 onClick={collapseExpand}>
61 <SVGIcon
62 name={isCollapsed ? 'chevronDown' : 'chevronUp'}
63 iconClassName={iconClass}
64 />
65 <div className="conflict-title">
66 {this.getTitle(type, name)}
67 </div>
68 </div>
69 </GridItem>
70 <GridItem className="yours">
71 <Radio
72 name={'radio_' + conflictId}
73 checked={resolution === ResolutionTypes.YOURS}
74 value="yours"
75 onChange={() =>
76 this.setState({
77 resolution: ResolutionTypes.YOURS
78 })
79 }
80 data-test-id={'radio_' + conflictId + '_yours'}
81 />
82 </GridItem>
83 <GridItem className="theirs">
84 <Radio
85 name={'radio_' + conflictId}
86 checked={resolution === ResolutionTypes.THEIRS}
87 value="theirs"
88 onChange={() =>
89 this.setState({
90 resolution: ResolutionTypes.THEIRS
91 })
92 }
93 data-test-id={'radio_' + conflictId + '_theirs'}
94 />
95 </GridItem>
96 <GridItem className="resolve">
97 <Button
98 className="conflict-resolve-btn"
Einav Weiss Keidar1801b242018-08-13 16:19:46 +030099 btnType="secondary"
100 size="default"
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200101 onClick={() =>
102 onResolveConflict({
103 conflictId,
104 resolution,
105 itemId,
106 version
107 })
108 }>
109 {i18n('Resolve')}
110 </Button>
111 </GridItem>
112 </GridSection>
113 <div>{isCollapsed && this.props.children}</div>
114 </div>
115 );
116 }
117}
talig8e9c0652017-12-20 14:30:43 +0200118
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200119class TextCompare extends React.Component {
120 render() {
121 // let rand = Math.random() * (3000 - 1) + 1;
122 let {
123 yours,
124 theirs,
125 field,
126 type,
127 isObjName,
128 conflictsOnly
129 } = this.props;
130 let typeYours = typeof yours;
131 let typeTheirs = typeof theirs;
talig8e9c0652017-12-20 14:30:43 +0200132
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200133 let parsedType = `${type}/${field}`.replace(/\/[0-9]+/g, '/index');
134 let level = type.split('/').length;
talig8e9c0652017-12-20 14:30:43 +0200135
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200136 if (typeYours === 'boolean' || typeTheirs === 'boolean') {
137 yours = yours ? i18n('Yes') : i18n('No');
138 theirs = theirs ? i18n('Yes') : i18n('No');
139 }
talig8e9c0652017-12-20 14:30:43 +0200140
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200141 /*if ((typeYours !== 'string' && typeYours !== 'undefined') || (typeTheirs !== 'string' && typeTheirs !== 'undefined')) {
talig8e9c0652017-12-20 14:30:43 +0200142 return (<div className='merge-editor-text-field field-error'>{field} cannot be parsed for display</div>);
143 }*/
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200144 let isDiff = yours !== theirs;
145 if (
146 !isObjName &&
147 ((!isDiff && conflictsOnly) ||
148 (yours === '' && theirs === '') ||
149 (typeYours === 'undefined' && typeTheirs === 'undefined'))
150 ) {
151 return null;
152 }
talig8e9c0652017-12-20 14:30:43 +0200153
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200154 return (
155 <GridSection
156 className={
157 isDiff
158 ? 'merge-editor-text-field diff'
159 : 'merge-editor-text-field'
160 }>
161 <GridItem className="field-col grid-col-title" stretch>
162 <div
163 className={`field ${
164 isDiff ? 'diff' : ''
165 } field-name level-${level} ${
166 isObjName ? 'field-object-name' : ''
167 }`}>
168 {i18n(parsedType)}
169 </div>
170 </GridItem>
171 <GridItem className="field-col grid-col-yours" stretch>
172 <div
173 className={`field field-yours ${
174 !yours ? 'empty-field' : ''
175 }`}>
176 {yours || (isObjName ? '' : '━━')}
177 </div>
178 </GridItem>
179 <GridItem className="field-col grid-col-theirs" stretch>
180 <div
181 className={`field field-theirs ${
182 !theirs ? 'empty-field' : ''
183 }`}>
184 {theirs || (isObjName ? '' : '━━')}
185 </div>
186 </GridItem>
187 <GridItem stretch />
188 </GridSection>
189 );
190 }
191}
talig8e9c0652017-12-20 14:30:43 +0200192
193class MergeEditorView extends React.Component {
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200194 state = {
195 collapsingSections: {},
196 conflictsOnly: false
197 };
talig8e9c0652017-12-20 14:30:43 +0200198
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200199 render() {
200 let {
201 conflicts,
202 item,
203 conflictFiles,
204 onResolveConflict,
205 currentScreen,
206 resolution
207 } = this.props;
talig8e9c0652017-12-20 14:30:43 +0200208
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200209 return (
210 <div className="merge-editor">
211 {conflictFiles && this.renderConflictTableTitles()}
212 <div className="merge-editor-body">
213 {conflictFiles &&
214 conflictFiles
215 .sort((a, b) => a.type > b.type)
216 .map(file => (
217 <ConflictCategory
218 key={'conflict_' + file.id}
219 conflict={file}
220 item={item}
221 isCollapsed={
222 this.state.collapsingSections[file.id]
223 }
224 collapseExpand={() => {
225 this.updateCollapseState(file.id);
226 }}
227 onResolveConflict={cDetails =>
228 onResolveConflict({
229 ...cDetails,
230 currentScreen
231 })
232 }>
233 {conflicts &&
234 conflicts[file.id] &&
235 this.getUnion(
236 conflicts[file.id].yours,
237 conflicts[file.id].theirs
238 ).map(field => {
239 return this.renderField(
240 field,
241 file,
242 conflicts[file.id].yours[field],
243 conflicts[file.id].theirs[
244 field
245 ],
246 resolution
247 );
248 })}
249 </ConflictCategory>
250 ))}
251 </div>
252 </div>
253 );
254 }
talig8e9c0652017-12-20 14:30:43 +0200255
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200256 renderConflictTableTitles() {
257 return (
258 <GridSection className="conflict-titles-section">
259 <GridItem>{i18n('Page')}</GridItem>
260 <GridItem className="yours">{i18n('Local (Me)')}</GridItem>
261 <GridItem className="theirs">{i18n('Last Committed')}</GridItem>
262 <GridItem className="resolve">
263 <Input
264 label={i18n('Show Conflicts Only')}
265 type="checkbox"
266 value={this.state.conflictsOnly}
267 onChange={e => this.setState({ conflictsOnly: e })}
268 />
269 </GridItem>
270 </GridSection>
271 );
272 }
273 // <Checkbox
274 // label={i18n('Show Conflicts Only')}
275 // value={this.state.conflictsOnly}
276 // checked={this.state.conflictsOnly}
277 // onChange={checked => this.setState({conflictsOnly: checked})} />
talig8e9c0652017-12-20 14:30:43 +0200278
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200279 renderObjects(yours, theirs, fileType, field, id, resolution) {
280 if (equal(yours, theirs)) {
281 return;
282 }
283 let { conflictsOnly } = this.state;
284 return (
285 <div key={`obj_${fileType}/${field}_${id}`}>
286 <TextCompare
287 field={field}
288 type={fileType}
289 conflictsOnly={conflictsOnly}
290 yours=""
291 theirs=""
292 isObjName
293 resolution={resolution}
294 />
295 <div className="field-objects">
296 <div>
297 {this.getUnion(yours, theirs).map(key =>
298 this.renderField(
299 key,
300 { type: `${fileType}/${field}`, id },
301 yours && yours[key],
302 theirs && theirs[key]
303 )
304 )}
305 </div>
306 </div>
307 </div>
308 );
309 }
talig8e9c0652017-12-20 14:30:43 +0200310
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200311 renderList(yours = [], theirs = [], type, field, id, resolution) {
312 let theirsList = theirs.join(', ');
313 let yoursList = yours.join(', ');
314 let { conflictsOnly } = this.state;
315 return (
316 <TextCompare
317 key={'text_' + id + '_' + field}
318 field={field}
319 type={type}
320 yours={yoursList}
321 theirs={theirsList}
322 conflictsOnly={conflictsOnly}
323 resolution={resolution}
324 />
325 );
326 }
talig8e9c0652017-12-20 14:30:43 +0200327
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200328 renderField(field, file, yours, theirs, resolution) {
329 if (yours) {
330 if (Array.isArray(yours)) {
331 return this.renderList(
332 yours,
333 theirs,
334 file.type,
335 field,
336 file.id,
337 resolution
338 );
339 } else if (typeof yours === 'object') {
340 return this.renderObjects(
341 yours,
342 theirs,
343 file.type,
344 field,
345 file.id,
346 resolution
347 );
348 }
349 } else if (theirs) {
350 if (Array.isArray(theirs)) {
351 return this.renderList(
352 yours,
353 theirs,
354 file.type,
355 field,
356 file.id,
357 resolution
358 );
359 } else if (typeof theirs === 'object') {
360 return this.renderObjects(
361 yours,
362 theirs,
363 file.type,
364 field,
365 file.id,
366 resolution
367 );
368 }
369 }
370 let { conflictsOnly } = this.state;
371 return (
372 <TextCompare
373 key={'text_' + file.id + '_' + field}
374 resolution={resolution}
375 field={field}
376 type={file.type}
377 yours={yours}
378 theirs={theirs}
379 conflictsOnly={conflictsOnly}
380 />
381 );
382 }
talig8e9c0652017-12-20 14:30:43 +0200383
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200384 getUnion(yours = {}, theirs = {}) {
385 let yoursKeys = Object.keys(yours);
386 let theirsKeys = Object.keys(theirs);
387 let myUn = union(yoursKeys, theirsKeys);
388 return myUn; //.sort((a, b) => a > b);
389 }
talig8e9c0652017-12-20 14:30:43 +0200390
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200391 updateCollapseState(conflictId) {
392 const {
393 fetchConflict,
394 item: { id: itemId, version } /*conflicts*/
395 } = this.props;
396 let isCollapsed = this.state.collapsingSections[conflictId];
397 // if (!isCollapsed && !(conflicts && conflictId in conflicts)) {
398 if (!isCollapsed) {
399 fetchConflict({ cid: conflictId, itemId, version });
400 }
401 this.setState({
402 collapsingSections: {
403 ...this.state.collapsingSections,
404 [conflictId]: !isCollapsed
405 }
406 });
407 }
talig8e9c0652017-12-20 14:30:43 +0200408}
409
410export default MergeEditorView;