Skip to content

Commit a84bad9

Browse files
committed
fix(sync): make server conflict resolution explicit
1 parent ffa2fe0 commit a84bad9

4 files changed

Lines changed: 116 additions & 6 deletions

File tree

src/GraphWorkspace.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const GraphComp = (props) => {
8484
isOpen={superState.confirmModal.open}
8585
title="Confirm"
8686
message={superState.confirmModal.message}
87+
actions={superState.confirmModal.actions}
8788
onConfirm={() => {
8889
if (superState.confirmModal.onConfirm) superState.confirmModal.onConfirm();
8990
dispatcher({ type: T.SET_CONFIRM_MODAL, payload: { open: false, message: '', onConfirm: null } });

src/component/modals/ConfirmModal.jsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,34 @@ import ParentModal from './ParentModal';
33
import './confirmModal.css';
44

55
const ConfirmModal = ({
6-
isOpen, title, message, onConfirm, onCancel,
6+
isOpen, title, message, onConfirm, onCancel, actions,
77
}) => (
88
<ParentModal ModelOpen={isOpen} closeModal={onCancel} title={title}>
99
<div className="confirm-modal-content">
1010
<div className="confirm-modal-message">{message}</div>
1111
<div className="confirm-modal-actions">
12-
<button type="button" className="confirm-btn" onClick={onConfirm}>Yes</button>
13-
<button type="button" className="cancel-btn" onClick={onCancel}>No</button>
12+
{
13+
actions && actions.length
14+
? actions.map((action) => (
15+
<button
16+
key={action.label}
17+
type="button"
18+
className={action.className || 'cancel-btn'}
19+
onClick={() => {
20+
if (action.onClick) action.onClick();
21+
onCancel();
22+
}}
23+
>
24+
{action.label}
25+
</button>
26+
))
27+
: (
28+
<>
29+
<button type="button" className="confirm-btn" onClick={onConfirm}>Yes</button>
30+
<button type="button" className="cancel-btn" onClick={onCancel}>No</button>
31+
</>
32+
)
33+
}
1434
</div>
1535
</div>
1636
</ParentModal>

src/component/modals/confirmModal.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
.confirm-modal-message {
66
margin-bottom: 20px;
77
font-size: 1.1em;
8+
white-space: pre-line;
89
}
910
.confirm-modal-actions {
1011
display: flex;

src/graph-builder/graph-core/6-server.js

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { toast } from 'react-toastify';
22
import Axios from 'axios';
33
import { actionType as T } from '../../reducer';
4-
import { EXECUTION_ENGINE_URL } from '../../serverCon/config';
4+
import serverConConfig, { EXECUTION_ENGINE_URL } from '../../serverCon/config';
55
import GraphLoadSave from './5-load-save';
66
// import {
77
// postGraph, updateGraph, forceUpdateGraph, getGraph, getGraphWithHashCheck,
@@ -11,6 +11,86 @@ import {
1111
} from '../../serverCon/crud_http';
1212

1313
class GraphServer extends GraphLoadSave {
14+
static isSyncConflictError(err) {
15+
const msg = (err && err.message ? err.message : '').toLowerCase();
16+
return msg.includes('different history')
17+
|| msg.includes('latest changes')
18+
|| msg.includes('can not update');
19+
}
20+
21+
showSyncConflictModal(reason) {
22+
const localHash = this.actionArr.length ? this.actionArr.at(-1).hash : 'None';
23+
const setModal = (remoteHash) => {
24+
const message = [
25+
reason || 'Sync conflict detected.',
26+
`Local hash: ${localHash}`,
27+
`Remote hash: ${remoteHash || 'Remote history is newer/different'}`,
28+
'Choose how to resolve this conflict.',
29+
].join('\n');
30+
this.dispatcher({
31+
type: T.SET_CONFIRM_MODAL,
32+
payload: {
33+
open: true,
34+
message,
35+
actions: [
36+
{
37+
label: 'Pull remote',
38+
className: 'confirm-btn',
39+
onClick: () => this.forcePullFromServer(),
40+
},
41+
{
42+
label: 'Force push local',
43+
className: 'confirm-btn',
44+
onClick: () => {
45+
if (this.serverID) {
46+
forceUpdateGraph(this.serverID, this.getGraphML()).catch((err) => {
47+
toast.error(err.response?.data?.message || err.message);
48+
});
49+
} else {
50+
postGraph(this.getGraphML()).then((serverID) => {
51+
this.set({ serverID });
52+
}).catch((err) => {
53+
toast.error(err.response?.data?.message || err.message);
54+
});
55+
}
56+
},
57+
},
58+
{
59+
label: 'Open remote in new tab',
60+
className: 'cancel-btn',
61+
onClick: () => {
62+
if (!this.serverID) return;
63+
const remotePath = serverConConfig.getGraph(this.serverID);
64+
const remoteURL = `${serverConConfig.baseURL}${remotePath}`;
65+
window.open(remoteURL, '_blank', 'noopener,noreferrer');
66+
},
67+
},
68+
{
69+
label: 'Cancel',
70+
className: 'cancel-btn',
71+
onClick: null,
72+
},
73+
],
74+
},
75+
});
76+
};
77+
if (!this.serverID) {
78+
setModal('Unknown');
79+
return;
80+
}
81+
getGraph(this.serverID).then((graphXML) => {
82+
try {
83+
const doc = new DOMParser().parseFromString(graphXML, 'application/xml');
84+
const hashes = doc.getElementsByTagName('hash');
85+
setModal(hashes.length ? (hashes[hashes.length - 1].textContent || '') : '');
86+
} catch {
87+
setModal('Unavailable');
88+
}
89+
}).catch(() => {
90+
setModal('Unavailable');
91+
});
92+
}
93+
1494
set(config) {
1595
const { serverID } = config;
1696
super.set(config);
@@ -73,6 +153,10 @@ class GraphServer extends GraphLoadSave {
73153
updateGraph(this.serverID, this.getGraphML()).then(() => {
74154

75155
}).catch((err) => {
156+
if (GraphServer.isSyncConflictError(err)) {
157+
this.showSyncConflictModal('Cannot push: local and remote histories diverged.');
158+
return;
159+
}
76160
toast.error(err.response?.data?.message || err.message);
77161
});
78162
} else {
@@ -127,8 +211,12 @@ class GraphServer extends GraphLoadSave {
127211
if (this.serverID) {
128212
getGraphWithHashCheck(this.serverID, this.actionArr.at(-1).hash).then((graphXML) => {
129213
this.setGraphML(graphXML);
130-
}).catch(() => {
131-
214+
}).catch((err) => {
215+
if (GraphServer.isSyncConflictError(err)) {
216+
this.showSyncConflictModal('Cannot pull: local and remote histories diverged.');
217+
return;
218+
}
219+
toast.error(err.response?.data?.message || err.message);
132220
});
133221
} else {
134222
toast.success('Not on server');

0 commit comments

Comments
 (0)