Skip to content

Commit 6dafcce

Browse files
fix: stabilize drawing actions and tag creation
1 parent 29f1eef commit 6dafcce

7 files changed

Lines changed: 269 additions & 162 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [0.6.9](https://github.com/programinglive/todo/compare/v0.6.8...v0.6.9) (2025-11-02)
6+
7+
8+
### 🐛 Bug Fixes
9+
10+
* ensure tag creation uses axios with credentials to avoid intermittent 419 errors and adjust todo form ordering
11+
* move drawing page navigation buttons outside the canvas container and gate debug logs behind the toggle
12+
13+
14+
### ✅ Tests
15+
16+
* add regression coverage for JSON tag create/update flows
17+
518
### [0.6.8](https://github.com/programinglive/todo/compare/v0.6.7...v0.6.8) (2025-11-02)
619

720

resources/js/Components/TagSelector.jsx

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -64,47 +64,43 @@ export default function TagSelector({ availableTags, selectedTagIds, onTagsChang
6464
setError('');
6565

6666
try {
67-
const response = await fetch('/manage/tags', {
68-
method: 'POST',
69-
headers: {
70-
'Content-Type': 'application/json',
71-
'Accept': 'application/json',
72-
'X-Requested-With': 'XMLHttpRequest',
73-
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
74-
},
75-
body: JSON.stringify({
67+
const response = await window.axios.post(
68+
'/manage/tags',
69+
{
7670
name: newTagName.trim(),
7771
color: newTagColor,
78-
}),
79-
});
80-
81-
if (response.ok) {
82-
const newTag = await response.json();
83-
84-
// Add the new tag to selected tags (only if it was newly created)
85-
const newTagKey = normalizeId(newTag.id);
86-
if (response.status === 201 || !selectedIdSet.has(newTagKey)) {
87-
onTagsChange([...safeSelectedIds, newTag.id]);
72+
},
73+
{
74+
headers: {
75+
Accept: 'application/json',
76+
},
8877
}
89-
90-
// Reset form
91-
setNewTagName('');
92-
setNewTagColor('#3B82F6');
93-
setIsCreating(false);
94-
setError('');
95-
96-
// Refresh the page to get updated tags list
97-
router.reload({ only: ['tags'] });
98-
} else if (response.status === 409) {
99-
// Conflict - tag already exists
100-
const errorData = await response.json().catch(() => ({}));
101-
setError(errorData.message || 'A tag with this name already exists');
102-
} else {
103-
const errorData = await response.json().catch(() => ({}));
104-
setError(errorData.message || 'Failed to create tag. Please try again.');
78+
);
79+
80+
const newTag = response.data;
81+
82+
// Add the new tag to selected tags (only if it was newly created)
83+
const newTagKey = normalizeId(newTag.id);
84+
if (response.status === 201 || !selectedIdSet.has(newTagKey)) {
85+
onTagsChange([...safeSelectedIds, newTag.id]);
10586
}
87+
88+
// Reset form
89+
setNewTagName('');
90+
setNewTagColor('#3B82F6');
91+
setIsCreating(false);
92+
setError('');
93+
94+
// Refresh the page to get updated tags list
95+
router.reload({ only: ['tags'] });
10696
} catch (error) {
107-
setError('Network error. Please check your connection and try again.');
97+
if (error.response?.status === 409) {
98+
setError(error.response.data?.message || 'A tag with this name already exists');
99+
} else if (error.response?.data?.message) {
100+
setError(error.response.data.message);
101+
} else {
102+
setError('Network error. Please check your connection and try again.');
103+
}
108104
} finally {
109105
setIsCreatingTag(false);
110106
}

resources/js/Pages/Draw/Index.jsx

Lines changed: 90 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const debugError = (...args) => {
4444

4545
// Simple production logger for important events only
4646
const prodLog = (...args) => {
47-
if (!isDebugMode()) {
47+
if (isDebugMode()) {
4848
console.log('[ZETTLY]', ...args);
4949
}
5050
};
@@ -805,26 +805,39 @@ export default function DrawIndex({ drawings: initialDrawings = [] }) {
805805
);
806806

807807
const handleCreateDrawing = useCallback(async () => {
808-
if (!editorRef.current) {
809-
return;
810-
}
811-
812808
await flushPendingSave();
813809
setCreating(true);
814810

815811
try {
816812
const title = `Untitled sketch ${drawings.length + 1}`;
817-
const baseSnapshot = blankSnapshotRef.current ?? editorRef.current.getSnapshot();
818-
const defaultSnapshot = normalizeSnapshotForPersist(baseSnapshot, title);
813+
814+
// If editor is mounted, use its snapshot; otherwise send minimal document
815+
let documentPayload;
816+
if (editorRef.current) {
817+
const baseSnapshot = blankSnapshotRef.current ?? editorRef.current.getSnapshot();
818+
const defaultSnapshot = normalizeSnapshotForPersist(baseSnapshot, title);
819+
documentPayload = normalizeSnapshotForPersist(defaultSnapshot, title);
820+
} else {
821+
// Create minimal blank document structure for new drawing
822+
documentPayload = {
823+
document: {
824+
name: title,
825+
store: {}
826+
}
827+
};
828+
}
829+
819830
const { data } = await window.axios.post(route('draw.store'), {
820831
title,
821-
document: normalizeSnapshotForPersist(defaultSnapshot, title),
832+
document: documentPayload,
822833
});
823834

824835
drawingCacheRef.current.set(data.drawing.id, data.drawing);
825836
setDrawings((prev) => [data.drawing, ...prev]);
826837
setActiveDrawing(data.drawing);
827-
loadDrawingIntoEditor(data.drawing);
838+
839+
// Navigate to the new drawing
840+
router.get(`/draw/${data.drawing.id}`);
828841
} catch (error) {
829842
debugError(error);
830843
setSaveStatus((prev) => ({
@@ -834,7 +847,7 @@ export default function DrawIndex({ drawings: initialDrawings = [] }) {
834847
} finally {
835848
setCreating(false);
836849
}
837-
}, [drawings.length, loadDrawingIntoEditor]);
850+
}, [drawings.length]);
838851

839852
// Autosave status for title
840853
const [titleSaveStatus, setTitleSaveStatus] = useState({ saving: false, lastSaved: null });
@@ -1316,7 +1329,7 @@ export default function DrawIndex({ drawings: initialDrawings = [] }) {
13161329
{drawings.length} {drawings.length === 1 ? 'drawing' : 'drawings'}
13171330
</p>
13181331
</div>
1319-
<Button onClick={() => router.get('/draw/create')} disabled={creating}>
1332+
<Button onClick={handleCreateDrawing} disabled={creating}>
13201333
{creating ? (
13211334
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
13221335
) : (
@@ -1337,17 +1350,9 @@ export default function DrawIndex({ drawings: initialDrawings = [] }) {
13371350
No drawings yet
13381351
</h3>
13391352
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
1340-
Create your first drawing to get started.
1353+
Use the New Drawing button above to get started.
13411354
</p>
13421355
</div>
1343-
<Button onClick={() => router.get('/draw/create')} disabled={creating}>
1344-
{creating ? (
1345-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
1346-
) : (
1347-
<Plus className="mr-2 h-4 w-4" />
1348-
)}
1349-
Create drawing
1350-
</Button>
13511356
</div>
13521357
) : (
13531358
<DrawingGallery
@@ -1382,6 +1387,36 @@ export default function DrawIndex({ drawings: initialDrawings = [] }) {
13821387
</p>
13831388
</div>
13841389

1390+
<div className="flex flex-wrap items-center gap-3">
1391+
<Button
1392+
variant="outline"
1393+
onClick={() => router.get('/draw')}
1394+
className="lg:w-auto"
1395+
>
1396+
← Back to Gallery
1397+
</Button>
1398+
1399+
{process.env.NODE_ENV === 'development' && (
1400+
<Button
1401+
variant="secondary"
1402+
onClick={() => {
1403+
const editor = editorRef.current;
1404+
if (editor && activeDrawing?.id) {
1405+
debugLog('[Draw] 🔧 Manual test: Triggering save');
1406+
const snapshot = editor.getSnapshot();
1407+
console.log('🔧 Manual test - Current snapshot:', snapshot);
1408+
queueSave(snapshot);
1409+
} else {
1410+
console.log('❌ Manual test failed - no editor or drawing');
1411+
}
1412+
}}
1413+
className="lg:w-auto text-xs"
1414+
>
1415+
🔧 Test Save
1416+
</Button>
1417+
)}
1418+
</div>
1419+
13851420
<div className="grid gap-6 lg:grid-cols-1">
13861421
<Card className="flex h-[75vh] flex-col overflow-hidden">
13871422
<CardHeader className="space-y-4 border-b border-gray-100 pt-4 pb-4 dark:border-slate-800">
@@ -1392,75 +1427,42 @@ export default function DrawIndex({ drawings: initialDrawings = [] }) {
13921427
</CardTitle>
13931428
{statusBadge}
13941429
</div>
1395-
<div className="flex w-full flex-col gap-2 lg:w-auto lg:flex-row lg:items-center">
1396-
{/* Back to Gallery */}
1397-
<Button
1398-
variant="outline"
1399-
onClick={() => router.get('/draw')}
1400-
className="lg:w-auto"
1401-
>
1402-
← Back to Gallery
1403-
</Button>
1404-
1405-
{/* Debug Test Button */}
1406-
{process.env.NODE_ENV === 'development' && (
1407-
<Button
1408-
variant="secondary"
1409-
onClick={() => {
1410-
const editor = editorRef.current;
1411-
if (editor && activeDrawing?.id) {
1412-
debugLog('[Draw] 🔧 Manual test: Triggering save');
1413-
const snapshot = editor.getSnapshot();
1414-
console.log('🔧 Manual test - Current snapshot:', snapshot);
1415-
queueSave(snapshot);
1416-
} else {
1417-
console.log('❌ Manual test failed - no editor or drawing');
1418-
}
1419-
}}
1420-
className="lg:w-auto text-xs"
1421-
>
1422-
🔧 Test Save
1423-
</Button>
1424-
)}
1425-
1426-
{/* Title Input */}
1427-
{activeDrawing ? (
1428-
<div className="flex flex-col gap-1">
1429-
<div className="flex items-center gap-2">
1430-
<label
1431-
htmlFor="drawing-title"
1432-
className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
1433-
>
1434-
Title
1435-
</label>
1436-
{titleSaveStatus.saving && (
1437-
<div className="flex items-center gap-1 text-xs text-blue-500">
1438-
<Loader2 className="h-3 w-3 animate-spin" />
1439-
Saving...
1440-
</div>
1441-
)}
1442-
{!titleSaveStatus.saving && titleSaveStatus.lastSaved && (
1443-
<div className="flex items-center gap-1 text-xs text-green-500">
1444-
<Check className="h-3 w-3" />
1445-
Saved
1446-
</div>
1447-
)}
1448-
</div>
1449-
<Input
1450-
id="drawing-title"
1451-
value={titleDraft}
1452-
onChange={(event) => {
1453-
setTitleDraft(event.target.value);
1454-
autoSaveDrawingTitle(event.target.value);
1455-
}}
1456-
onBlur={handleTitleBlur}
1457-
onKeyDown={handleTitleKeyDown}
1458-
className="lg:w-64"
1459-
placeholder="Name your drawing"
1460-
/>
1430+
{activeDrawing ? (
1431+
<div className="flex w-full flex-col gap-2 text-sm lg:w-auto lg:flex-row lg:items-center">
1432+
<div className="flex items-center gap-2">
1433+
<label
1434+
htmlFor="drawing-title"
1435+
className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
1436+
>
1437+
Title
1438+
</label>
1439+
{titleSaveStatus.saving && (
1440+
<div className="flex items-center gap-1 text-xs text-blue-500">
1441+
<Loader2 className="h-3 w-3 animate-spin" />
1442+
Saving...
1443+
</div>
1444+
)}
1445+
{!titleSaveStatus.saving && titleSaveStatus.lastSaved && (
1446+
<div className="flex items-center gap-1 text-xs text-green-500">
1447+
<Check className="h-3 w-3" />
1448+
Saved
1449+
</div>
1450+
)}
14611451
</div>
1462-
) : null}
1463-
</div>
1452+
<Input
1453+
id="drawing-title"
1454+
value={titleDraft}
1455+
onChange={(event) => {
1456+
setTitleDraft(event.target.value);
1457+
autoSaveDrawingTitle(event.target.value);
1458+
}}
1459+
onBlur={handleTitleBlur}
1460+
onKeyDown={handleTitleKeyDown}
1461+
className="lg:w-64"
1462+
placeholder="Name your drawing"
1463+
/>
1464+
</div>
1465+
) : null}
14641466
</div>
14651467
</CardHeader>
14661468
<CardContent className="relative flex-1 overflow-hidden p-0">

0 commit comments

Comments
 (0)