fix: ScriptFixerDialog no longer overwrites unrelated scripts (Vibe Kanban) (#2078)

* All tasks are complete. Here's a summary of the changes made:

## Summary

Fixed the issue where `ScriptFixerDialog.tsx` was replacing cleanup and setup scripts with `null` when editing a dev server script.

### Changes Made:

1. **Added `serde_with` dependency** (`Cargo.toml` and `crates/db/Cargo.toml`)
   - Added `serde_with = "3"` to enable the double option pattern

2. **Updated `UpdateRepo` struct** (`crates/db/src/models/repo.rs`)
   - Changed all fields from `Option<T>` to `Option<Option<T>>` with the `double_option` serde helper
   - Added `#[ts(optional, type = "...")]` annotations for proper TypeScript type generation

3. **Updated `Repo::update` method** (`crates/db/src/models/repo.rs`)
   - Now properly handles three states:
     - `None` = field absent → preserve existing value
     - `Some(None)` = explicitly null → set to NULL
     - `Some(Some(v))` = has value → update to value

4. **Updated `ScriptFixerDialog.tsx`** (`frontend/src/components/dialogs/scripts/ScriptFixerDialog.tsx`)
   - Changed `handleSave` and `handleSaveAndTest` to only send the field being edited
   - Removed `repos` from the dependency arrays since it's no longer used in the save functions

5. **Regenerated TypeScript types**
   - `UpdateRepo` now has all fields as optional: `field?: type | null`

### Result:
The API now supports true partial updates. Clients can send only the fields they want to update, and existing values are preserved for omitted fields. Sending `null` explicitly will clear a field to NULL.

* Done. All Rust code has been formatted.
This commit is contained in:
Louis Knight-Webb
2026-01-15 15:49:01 +00:00
committed by GitHub
parent 75beab35d9
commit ede1511a50
6 changed files with 99 additions and 68 deletions

View File

@@ -166,27 +166,16 @@ const ScriptFixerDialogImpl = NiceModal.create<ScriptFixerDialogProps>(
setError(null);
try {
// Build update data with all required fields
const selectedRepo = repos.find((r) => r.id === selectedRepoId);
const updateData: UpdateRepo = {
display_name: selectedRepo?.display_name ?? null,
setup_script:
scriptType === 'setup'
? script.trim() || null
: (selectedRepo?.setup_script ?? null),
cleanup_script:
scriptType === 'cleanup'
? script.trim() || null
: (selectedRepo?.cleanup_script ?? null),
copy_files: selectedRepo?.copy_files ?? null,
parallel_setup_script: selectedRepo?.parallel_setup_script ?? null,
dev_server_script:
scriptType === 'dev_server'
? script.trim() || null
: (selectedRepo?.dev_server_script ?? null),
};
// Only send the field being edited - other fields will be preserved by the backend
const scriptValue = script.trim() || null;
const updateData: Partial<UpdateRepo> =
scriptType === 'setup'
? { setup_script: scriptValue }
: scriptType === 'cleanup'
? { cleanup_script: scriptValue }
: { dev_server_script: scriptValue };
await repoApi.update(selectedRepoId, updateData);
await repoApi.update(selectedRepoId, updateData as UpdateRepo);
// Invalidate repos cache
queryClient.invalidateQueries({ queryKey: ['repos'] });
@@ -201,7 +190,7 @@ const ScriptFixerDialogImpl = NiceModal.create<ScriptFixerDialogProps>(
} finally {
setIsSaving(false);
}
}, [selectedRepoId, script, scriptType, queryClient, modal, t, repos]);
}, [selectedRepoId, script, scriptType, queryClient, modal, t]);
const handleSaveAndTest = useCallback(async () => {
if (!selectedRepoId) return;
@@ -210,27 +199,16 @@ const ScriptFixerDialogImpl = NiceModal.create<ScriptFixerDialogProps>(
setError(null);
try {
// First save the script
const selectedRepo = repos.find((r) => r.id === selectedRepoId);
const updateData: UpdateRepo = {
display_name: selectedRepo?.display_name ?? null,
setup_script:
scriptType === 'setup'
? script.trim() || null
: (selectedRepo?.setup_script ?? null),
cleanup_script:
scriptType === 'cleanup'
? script.trim() || null
: (selectedRepo?.cleanup_script ?? null),
copy_files: selectedRepo?.copy_files ?? null,
parallel_setup_script: selectedRepo?.parallel_setup_script ?? null,
dev_server_script:
scriptType === 'dev_server'
? script.trim() || null
: (selectedRepo?.dev_server_script ?? null),
};
// Only send the field being edited - other fields will be preserved by the backend
const scriptValue = script.trim() || null;
const updateData: Partial<UpdateRepo> =
scriptType === 'setup'
? { setup_script: scriptValue }
: scriptType === 'cleanup'
? { cleanup_script: scriptValue }
: { dev_server_script: scriptValue };
await repoApi.update(selectedRepoId, updateData);
await repoApi.update(selectedRepoId, updateData as UpdateRepo);
// Invalidate repos cache
queryClient.invalidateQueries({ queryKey: ['repos'] });
@@ -256,15 +234,7 @@ const ScriptFixerDialogImpl = NiceModal.create<ScriptFixerDialogProps>(
} finally {
setIsTesting(false);
}
}, [
selectedRepoId,
script,
scriptType,
workspaceId,
queryClient,
t,
repos,
]);
}, [selectedRepoId, script, scriptType, workspaceId, queryClient, t]);
const dialogTitle =
scriptType === 'setup'