feat: Implement LLM provider deletion and refactor ConfigManager to preserve unknown payload sections.
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
let testStatus = $state({ type: "", message: "" });
|
||||
let isTesting = $state(false);
|
||||
let togglingProviderIds = $state(new Set());
|
||||
let deletingProviderIds = $state(new Set());
|
||||
|
||||
function isMultimodalModel(modelName) {
|
||||
const token = (modelName || "").toLowerCase();
|
||||
@@ -155,6 +156,38 @@
|
||||
onSave();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(provider) {
|
||||
if (deletingProviderIds.has(provider.id)) return;
|
||||
if (
|
||||
!confirm(
|
||||
$t.llm.delete_confirm.replace("{name}", provider.name || provider.id),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
deletingProviderIds.add(provider.id);
|
||||
deletingProviderIds = new Set(deletingProviderIds);
|
||||
|
||||
try {
|
||||
await requestApi(`/llm/providers/${provider.id}`, "DELETE");
|
||||
providers = providers.filter(({ id }) => id !== provider.id);
|
||||
addToast($t.llm.delete_success.replace("{name}", provider.name), "success");
|
||||
onSave();
|
||||
} catch (err) {
|
||||
addToast(
|
||||
$t.llm.delete_failed.replace(
|
||||
"{error}",
|
||||
err?.message || $t.common.error,
|
||||
),
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
deletingProviderIds.delete(provider.id);
|
||||
deletingProviderIds = new Set(deletingProviderIds);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-4">
|
||||
@@ -345,11 +378,23 @@
|
||||
>
|
||||
{$t.common.edit}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-red-600 hover:underline"
|
||||
onclick={() => handleDelete(provider)}
|
||||
disabled={deletingProviderIds.has(provider.id)}
|
||||
>
|
||||
{#if deletingProviderIds.has(provider.id)}
|
||||
...
|
||||
{:else}
|
||||
{$t.common.delete}
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`text-sm ${provider.is_active ? "text-orange-600" : "text-green-600"} hover:underline`}
|
||||
onclick={() => toggleActive(provider)}
|
||||
disabled={togglingProviderIds.has(provider.id)}
|
||||
disabled={togglingProviderIds.has(provider.id) || deletingProviderIds.has(provider.id)}
|
||||
>
|
||||
{#if togglingProviderIds.has(provider.id)}
|
||||
...
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// [DEF:frontend.src.components.llm.__tests__.provider_config_integration:Module]
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: llm, provider-config, integration-test, edit-flow
|
||||
// @PURPOSE: Protect edit-button interaction contract in LLM provider settings UI.
|
||||
// @SEMANTICS: llm, provider-config, integration-test, edit-flow, delete-flow
|
||||
// @PURPOSE: Protect edit and delete interaction contracts in LLM provider settings UI.
|
||||
// @LAYER: UI Tests
|
||||
// @RELATION: VERIFIES -> frontend/src/components/llm/ProviderConfig.svelte
|
||||
// @INVARIANT: Edit action keeps explicit click handler and opens normalized edit form.
|
||||
@@ -17,7 +17,7 @@ const COMPONENT_PATH = path.resolve(
|
||||
|
||||
// [DEF:provider_config_edit_contract_tests:Function]
|
||||
// @TIER: STANDARD
|
||||
// @PURPOSE: Validate edit button handler wiring and normalized edit form state mapping.
|
||||
// @PURPOSE: Validate edit and delete handler wiring plus normalized edit form state mapping.
|
||||
// @PRE: ProviderConfig component source exists in expected path.
|
||||
// @POST: Contract checks ensure edit click cannot degrade into no-op flow.
|
||||
describe('ProviderConfig edit interaction contract', () => {
|
||||
@@ -25,9 +25,7 @@ describe('ProviderConfig edit interaction contract', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain('type="button"');
|
||||
expect(source).toContain(
|
||||
"on:click|preventDefault|stopPropagation={() => handleEdit(provider)}",
|
||||
);
|
||||
expect(source).toContain('onclick={() => handleEdit(provider)}');
|
||||
});
|
||||
|
||||
it('normalizes provider payload into editable form shape', () => {
|
||||
@@ -39,6 +37,15 @@ describe('ProviderConfig edit interaction contract', () => {
|
||||
expect(source).toContain('default_model: provider?.default_model ?? "gpt-4o"');
|
||||
expect(source).toContain('showForm = true;');
|
||||
});
|
||||
|
||||
it('keeps explicit delete flow with confirmation and delete request', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain('async function handleDelete(provider)');
|
||||
expect(source).toContain('$t.llm.delete_confirm.replace("{name}", provider.name || provider.id)');
|
||||
expect(source).toContain('await requestApi(`/llm/providers/${provider.id}`, "DELETE")');
|
||||
expect(source).toContain('onclick={() => handleDelete(provider)}');
|
||||
});
|
||||
});
|
||||
// [/DEF:provider_config_edit_contract_tests:Function]
|
||||
// [/DEF:frontend.src.components.llm.__tests__.provider_config_integration:Module]
|
||||
|
||||
@@ -111,6 +111,9 @@
|
||||
"connection_success": "Connection successful!",
|
||||
"connection_failed": "Connection failed: {error}",
|
||||
"no_providers": "No providers configured.",
|
||||
"delete_confirm": "Delete LLM provider \"{name}\"?",
|
||||
"delete_success": "Provider \"{name}\" deleted.",
|
||||
"delete_failed": "Failed to delete provider: {error}",
|
||||
"multimodal": "Multimodal",
|
||||
"text_only": "Text only",
|
||||
"doc_preview_title": "Documentation Preview",
|
||||
|
||||
@@ -111,6 +111,9 @@
|
||||
"connection_success": "Подключение успешно!",
|
||||
"connection_failed": "Ошибка подключения: {error}",
|
||||
"no_providers": "Провайдеры не настроены.",
|
||||
"delete_confirm": "Удалить LLM-провайдера \"{name}\"?",
|
||||
"delete_success": "Провайдер \"{name}\" удалён.",
|
||||
"delete_failed": "Не удалось удалить провайдера: {error}",
|
||||
"multimodal": "Мультимодальная",
|
||||
"text_only": "Только текст",
|
||||
"doc_preview_title": "Предпросмотр документации",
|
||||
|
||||
Reference in New Issue
Block a user