-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConflictToast.tsx
More file actions
103 lines (93 loc) · 3.02 KB
/
Copy pathConflictToast.tsx
File metadata and controls
103 lines (93 loc) · 3.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
'use client';
import { useMemo } from 'react';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Snackbar from '@mui/material/Snackbar';
import Typography from '@mui/material/Typography';
import { useOptimisticStore } from '@/lib/realtime/useOptimisticStore';
import type { OptimisticConflictDetail } from '@/lib/realtime/optimistic';
export type ConflictOfferLookup = ReadonlyMap<string, { title: string }>;
type Props = {
offerLookup?: ConflictOfferLookup;
};
const FIELD_LABELS: Record<OptimisticConflictDetail['field'], string> = {
user_offer_activity_status: 'Status',
_deleted: 'Offer removed',
};
function formatValue(value: unknown): string {
if (value == null) return '—';
if (typeof value === 'string') return value;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
export function ConflictToast({ offerLookup }: Props) {
const { conflicts, resolve } = useOptimisticStore();
const active = conflicts[0] ?? null;
const title = useMemo(() => {
if (!active) return '';
const lookup = offerLookup?.get(active.offer_id);
return lookup?.title ?? 'this offer';
}, [active, offerLookup]);
if (!active) return null;
const detail = active.details[0];
const body =
detail?.field === '_deleted'
? `Someone removed ${title} while your change was in flight.`
: `${title} changed under you. ${FIELD_LABELS[detail.field] ?? detail.field}: you had ${formatValue(detail.local_value)}, theirs is ${formatValue(detail.remote_value)}.`;
return (
<Snackbar
open
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
sx={{ maxWidth: 520 }}
data-testid="conflict-toast"
>
<Alert
severity="warning"
variant="filled"
role="alert"
icon={false}
sx={{
alignItems: 'flex-start',
width: '100%',
boxShadow: 6,
'& .MuiAlert-message': { flex: 1, minWidth: 0 },
}}
action={
<Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
<Button
size="small"
variant="text"
color="inherit"
onClick={() => resolve(active.mutation_id, 'use_remote')}
sx={{ textTransform: 'none', fontWeight: 600 }}
>
Use theirs
</Button>
<Button
size="small"
variant="outlined"
color="inherit"
onClick={() => resolve(active.mutation_id, 'keep_local')}
sx={{
textTransform: 'none',
fontWeight: 600,
borderColor: 'rgba(255,255,255,0.5)',
}}
>
Keep my change
</Button>
</Box>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
Conflicting update
</Typography>
<Typography variant="body2">{body}</Typography>
</Alert>
</Snackbar>
);
}