Skip to content

Commit b905dff

Browse files
committed
fix: fixed intermediate states on edit statements
1 parent 5da4c21 commit b905dff

File tree

6 files changed

+209
-92
lines changed

6 files changed

+209
-92
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ allcode.txt
2727
.env.*
2828
.env.development
2929
.aider*
30+
31+
CLAUDE.md

src/components/MainPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Button } from './ui/button';
88
import { Plus, Mail } from 'lucide-react';
99
import StatementWizard from './statementWizard/StatementWizard';
1010
import ShareEmailModal from './ShareEmailModal'; // Import the new modal
11+
import TestStatementButton from './test/TestButton';
1112

1213
const MainPage: React.FC = () => {
1314
const { data } = useEntries();
@@ -71,6 +72,7 @@ const MainPage: React.FC = () => {
7172
<Plus className='w-6 h-6' />
7273
<span className='text-lg'>Create your own statement</span>
7374
</Button>
75+
<TestStatementButton />
7476
</div>
7577

7678
{/* Conditionally render the wizard modal */}

src/components/statementWizard/EditStatementModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const EditStatementModal: React.FC<EditStatementModalProps> = ({
6565
} else {
6666
updatedStatement = statement;
6767
}
68+
// Send the updated statement to the parent via onUpdate callback
6869
onUpdate(updatedStatement);
6970
onClose();
7071
};

src/components/statements/StatementItem.tsx

Lines changed: 107 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import React, { useRef, useEffect } from 'react';
2-
import { Input } from '../ui/input';
1+
import React, { useEffect } from 'react';
32
import { Button } from '../ui/button';
4-
import SubjectSelector from '../statementWizard/selectors/SubjectSelector';
5-
import VerbSelector from '../statementWizard/selectors/VerbSelector';
63
import { getVerbName } from '../../../utils/verbUtils';
74
import {
85
Trash2,
@@ -58,7 +55,6 @@ export interface StatementItemProps {
5855
const StatementItem: React.FC<StatementItemProps> = ({
5956
statement,
6057
isEditing,
61-
editingPart,
6258
onPartClick,
6359
onLocalSave,
6460
onDelete,
@@ -72,41 +68,76 @@ const StatementItem: React.FC<StatementItemProps> = ({
7268
onToggleActionResolved = () => {},
7369
}) => {
7470
const [isActionsExpanded, setIsActionsExpanded] = React.useState(false);
75-
const objectInputRef = useRef<HTMLInputElement>(null);
7671

7772
// Local "draft" state to hold unsaved modifications.
7873
const [draft, setDraft] = React.useState<Entry>(statement);
7974

80-
// Local state to track if we are currently saving the draft. Will control the save button.
75+
// initialDraft freezes the original values when editing begins.
76+
const [initialDraft, setInitialDraft] = React.useState<Entry>(statement);
77+
78+
// Local state to track if we are currently saving the draft.
8179
const [isSaving, setIsSaving] = React.useState(false);
82-
// Compute if there are any changes compared to the original statement prop.
83-
const hasChanged =
84-
draft.atoms.subject !== statement.atoms.subject ||
85-
draft.atoms.verb !== statement.atoms.verb ||
86-
draft.atoms.object !== statement.atoms.object ||
87-
draft.isPublic !== statement.isPublic;
8880

89-
// Whenever the statement prop changes (or when not editing), re-sync the draft.
81+
// First useEffect: Capture changes in edit mode status
9082
useEffect(() => {
91-
setDraft(statement);
92-
}, [statement]);
83+
// Create a deep copy of the statement to avoid reference issues
84+
const statementCopy = JSON.parse(JSON.stringify(statement));
85+
86+
if (isEditing) {
87+
// Only capture initial state when entering edit mode
88+
// This will be our reference point for comparison
89+
setInitialDraft(statementCopy);
90+
} else {
91+
// Reset both states when exiting edit mode
92+
setInitialDraft(statementCopy);
93+
setDraft(statementCopy);
94+
}
95+
// eslint-disable-next-line react-hooks/exhaustive-deps
96+
}, [isEditing]); // Intentionally excluding statement to avoid resetting initialDraft
9397

98+
// Second useEffect: Always keep draft updated with latest statement to reflect modal changes
9499
useEffect(() => {
95-
if (editingPart === 'object' && objectInputRef.current) {
96-
objectInputRef.current.focus();
100+
if (isEditing) {
101+
// When statement changes while in edit mode, update the draft
102+
// This happens when the modal updates the statement
103+
setDraft(JSON.parse(JSON.stringify(statement)));
97104
}
98-
}, [editingPart]);
105+
}, [
106+
statement,
107+
// We're specifically tracking these properties to ensure we detect changes
108+
// from the modal even if the statement reference doesn't change
109+
statement.atoms.subject,
110+
statement.atoms.verb,
111+
statement.atoms.object,
112+
statement.isPublic,
113+
isEditing,
114+
]);
99115

100-
// Local function to update a specific part in the draft.
116+
// Uncomment and use this if you need to update parts directly instead of using the modal
117+
/*
101118
const updatePart = (part: 'subject' | 'verb' | 'object', value: string) => {
102-
setDraft((prev) => ({
103-
...prev,
104-
atoms: {
105-
...prev.atoms,
106-
[part]: value,
107-
},
108-
}));
119+
// Create a new draft object to ensure React detects the change
120+
setDraft((prevDraft) => {
121+
const newDraft = JSON.parse(JSON.stringify(prevDraft));
122+
newDraft.atoms[part] = value;
123+
return newDraft;
124+
});
109125
};
126+
*/
127+
128+
// Compute if draft has changed from the initial state
129+
const hasSubjectChanged = draft.atoms.subject !== initialDraft.atoms.subject;
130+
const hasVerbChanged = draft.atoms.verb !== initialDraft.atoms.verb;
131+
const hasObjectChanged = draft.atoms.object !== initialDraft.atoms.object;
132+
const hasPrivacyChanged = draft.isPublic !== initialDraft.isPublic;
133+
134+
const hasChanged =
135+
hasSubjectChanged ||
136+
hasVerbChanged ||
137+
hasObjectChanged ||
138+
hasPrivacyChanged;
139+
140+
// Enable save button when any part of the statement has been changed
110141

111142
if (isEditing) {
112143
return (
@@ -116,11 +147,12 @@ const StatementItem: React.FC<StatementItemProps> = ({
116147
variant='ghost'
117148
size='sm'
118149
onClick={() => {
119-
console.log('Privacy toggle clicked for statement:', draft.id);
120-
setDraft((prev) => ({
121-
...prev,
122-
isPublic: !prev.isPublic,
123-
}));
150+
// Create a new draft object to ensure React detects the change
151+
setDraft((prevDraft) => {
152+
const newDraft = JSON.parse(JSON.stringify(prevDraft));
153+
newDraft.isPublic = !prevDraft.isPublic;
154+
return newDraft;
155+
});
124156
}}
125157
className={`rounded-md px-3 py-2 transition-colors ${
126158
draft.isPublic
@@ -134,55 +166,37 @@ const StatementItem: React.FC<StatementItemProps> = ({
134166
<div className='flex flex-1 items-center space-x-2'>
135167
{/* Subject */}
136168
<div
137-
onClick={() => onPartClick('subject', draft.id)}
169+
onClick={() => {
170+
// Just call onPartClick to open the modal, and don't try to edit inline
171+
onPartClick('subject', draft.id);
172+
}}
138173
className='cursor-pointer px-2 py-1 rounded bg-subjectSelector hover:bg-subjectSelectorHover'
139174
>
140-
{editingPart === 'subject' ? (
141-
<SubjectSelector
142-
value={draft.atoms.subject}
143-
onChange={(value) => updatePart('subject', value)}
144-
onAddDescriptor={() => {}}
145-
username={
146-
draft.atoms.subject.split("'s")[0] || draft.atoms.subject
147-
}
148-
/>
149-
) : (
150-
draft.atoms.subject
151-
)}
175+
{draft.atoms.subject}
152176
</div>
153177
{/* Verb */}
154178
<div
179+
onClick={() => {
180+
// Just call onPartClick to open the modal, and don't try to edit inline
181+
onPartClick('verb', draft.id);
182+
}}
155183
className='cursor-pointer px-2 py-1 rounded bg-verbSelector hover:bg-verbSelectorHover'
156-
onClick={() => onPartClick('verb', draft.id)}
157184
>
158-
{editingPart === 'verb' ? (
159-
<VerbSelector
160-
onVerbSelect={(verb) => updatePart('verb', verb.id)}
161-
onClose={() => onPartClick('verb', '')}
162-
/>
163-
) : (
164-
<span>{getVerbName(draft.atoms.verb)}</span>
165-
)}
185+
<span>{getVerbName(draft.atoms.verb)}</span>
166186
</div>
167187
{/* Object */}
168188
<div
169-
onClick={() => onPartClick('object', draft.id)}
189+
onClick={() => {
190+
// Just call onPartClick to open the modal, and don't try to edit inline
191+
onPartClick('object', draft.id);
192+
}}
170193
className='cursor-pointer px-2 py-1 rounded bg-objectInput hover:bg-objectInputHover'
171194
>
172-
{editingPart === 'object' ? (
173-
<Input
174-
ref={objectInputRef}
175-
value={draft.atoms.object}
176-
onChange={(e) => updatePart('object', e.target.value)}
177-
className='w-full'
178-
/>
179-
) : (
180-
draft.atoms.object
181-
)}
195+
{draft.atoms.object}
182196
</div>
183197
</div>
184198
<div className='flex items-center space-x-2 ml-auto'>
185-
{/* This button needs a span wrapper to always show the tooltip */}
199+
{/* Save button with tooltip */}
186200
<Tooltip>
187201
<TooltipTrigger asChild>
188202
<span className='inline-block'>
@@ -195,7 +209,7 @@ const StatementItem: React.FC<StatementItemProps> = ({
195209
setIsSaving(false);
196210
}}
197211
disabled={!hasChanged || isSaving}
198-
className='text-green-500 hover:text-green-700 px-2'
212+
className='text-green-500 hover:text-green-700 px-4 py-2'
199213
>
200214
<Save size={16} />
201215
</Button>
@@ -206,35 +220,42 @@ const StatementItem: React.FC<StatementItemProps> = ({
206220
</TooltipContent>
207221
</Tooltip>
208222

223+
{/* Cancel button with PenOff icon and tooltip */}
209224
<Tooltip>
210225
<TooltipTrigger asChild>
211-
<Button
212-
variant='ghost'
213-
size='sm'
214-
onClick={() => {
215-
setDraft(statement);
216-
if (onCancel) onCancel(statement.id);
217-
}}
218-
className='text-red-500 hover:text-red-700 px-2'
219-
>
220-
<PenOff size={16} />
221-
</Button>
226+
<span className='inline-block'>
227+
<Button
228+
variant='ghost'
229+
size='sm'
230+
onClick={() => {
231+
// Deep clone to avoid reference issues
232+
setDraft(JSON.parse(JSON.stringify(initialDraft)));
233+
if (onCancel) onCancel(statement.id);
234+
}}
235+
className='text-red-500 hover:text-red-700 px-4 py-2'
236+
>
237+
<PenOff size={16} />
238+
</Button>
239+
</span>
222240
</TooltipTrigger>
223241
<TooltipContent className='p-2 bg-black text-white rounded'>
224242
Cancel editing
225243
</TooltipContent>
226244
</Tooltip>
227245

246+
{/* Delete button with tooltip */}
228247
<Tooltip>
229248
<TooltipTrigger asChild>
230-
<Button
231-
variant='ghost'
232-
size='sm'
233-
onClick={() => onDelete(draft.id)}
234-
className='text-red-500 hover:text-red-700 px-2'
235-
>
236-
<Trash2 size={16} />
237-
</Button>
249+
<span className='inline-block'>
250+
<Button
251+
variant='ghost'
252+
size='sm'
253+
onClick={() => onDelete(draft.id)}
254+
className='text-red-500 hover:text-red-700 px-4 py-2'
255+
>
256+
<Trash2 size={16} />
257+
</Button>
258+
</span>
238259
</TooltipTrigger>
239260
<TooltipContent className='p-2 bg-black text-white rounded'>
240261
Delete statement
@@ -252,9 +273,7 @@ const StatementItem: React.FC<StatementItemProps> = ({
252273
statement.isResolved ? 'border-green-500' : 'border-gray-200'
253274
}`}
254275
>
255-
{/* Top row: statement, actions counter, etc. */}
256276
<div className='flex items-center justify-between'>
257-
{/* Left side: privacy icon + full statement text */}
258277
<div className='flex items-center space-x-2'>
259278
<Tooltip>
260279
<TooltipTrigger asChild>
@@ -280,8 +299,6 @@ const StatementItem: React.FC<StatementItemProps> = ({
280299
statement.atoms.verb
281300
)} ${statement.atoms.object}`}</span>
282301
</div>
283-
284-
{/* Right side: resolved icon, actions counter + dropdown */}
285302
<div className='flex items-center space-x-4'>
286303
{statement.isResolved && (
287304
<Tooltip>

0 commit comments

Comments
 (0)