diff --git a/web/src/lib/components/modules/ModuleAsEntriesCard.svelte b/web/src/lib/components/modules/ModuleAsEntriesCard.svelte index b47a619..8f236e9 100644 --- a/web/src/lib/components/modules/ModuleAsEntriesCard.svelte +++ b/web/src/lib/components/modules/ModuleAsEntriesCard.svelte @@ -46,8 +46,13 @@ let csvExporting = $state(false); let csvFileInput = $state(null); - const selectedCount = $derived(selectedIds.size); - const allSelected = $derived(entries.length > 0 && selectedIds.size === entries.length); + const activeSelected = $derived.by(() => { + const allowed = new Set(entries.map((e) => e.id)); + return [...selectedIds].filter((id) => allowed.has(id)); + }); + const selectedCount = $derived(activeSelected.length); + + const allSelected = $derived(entries.length > 0 && entries.every((e) => selectedIds.has(e.id))); const columns = [ { id: 'select', label: '', class: 'w-10' }, @@ -75,11 +80,6 @@ { id: 'actions', label: '', class: 'w-20' } ] as const; - $effect(() => { - const validIds = new Set(entries.map((e) => e.id)); - selectedIds = new Set([...selectedIds].filter((id) => validIds.has(id))); - }); - function toggleSelection(id: string) { const next = new Set(selectedIds); if (next.has(id)) next.delete(id); @@ -133,7 +133,7 @@ deletingBulk = true; let deleted = 0; try { - for (const id of selectedIds) { + for (const id of activeSelected) { try { await apiMutate(`/v1/modules/${moduleId}/as-entries/${id}`, 'DELETE', undefined, { idempotent: false diff --git a/web/src/lib/components/modules/ModuleAsEntryDialog.svelte b/web/src/lib/components/modules/ModuleAsEntryDialog.svelte index d8bc39b..5b1e55a 100644 --- a/web/src/lib/components/modules/ModuleAsEntryDialog.svelte +++ b/web/src/lib/components/modules/ModuleAsEntryDialog.svelte @@ -35,12 +35,23 @@ let saving = $state(false); let form = $state({ asn: 0, community_id: null }); + let initKey = $state(''); + + function resetForm() { + form = edit + ? { asn: edit.asn, community_id: edit.community_id } + : { asn: 0, community_id: null }; + } $effect(() => { - if (open) { - form = edit - ? { asn: edit.asn, community_id: edit.community_id } - : { asn: 0, community_id: null }; + if (!open) { + initKey = ''; + return; + } + const nextKey = edit?.id ?? 'new'; + if (nextKey !== initKey) { + initKey = nextKey; + resetForm(); } }); @@ -75,7 +86,7 @@ } - + {edit ? 'Редактировать запись' : 'Новая AS-запись'} diff --git a/web/src/lib/components/modules/ModuleCdnSourceDialog.svelte b/web/src/lib/components/modules/ModuleCdnSourceDialog.svelte index 3f82d2e..bc4011f 100644 --- a/web/src/lib/components/modules/ModuleCdnSourceDialog.svelte +++ b/web/src/lib/components/modules/ModuleCdnSourceDialog.svelte @@ -51,6 +51,7 @@ prefix_path: '', community_id: null }); + let initKey = $state(''); function clearPreview() { previewLoading = false; @@ -61,18 +62,28 @@ previewOk = false; } + function resetForm() { + clearPreview(); + form = edit + ? { + url: edit.url, + source_kind: normalizeCdnSourceKind(edit.source_kind), + prefix_path: edit.prefix_path ?? '', + community_id: edit.community_id, + refresh_interval_sec: edit.refresh_interval_sec + } + : { url: '', source_kind: 'plaintext', prefix_path: '', community_id: null }; + } + $effect(() => { - if (open) { - clearPreview(); - form = edit - ? { - url: edit.url, - source_kind: normalizeCdnSourceKind(edit.source_kind), - prefix_path: edit.prefix_path ?? '', - community_id: edit.community_id, - refresh_interval_sec: edit.refresh_interval_sec - } - : { url: '', source_kind: 'plaintext', prefix_path: '', community_id: null }; + if (!open) { + initKey = ''; + return; + } + const nextKey = edit?.id ?? 'new'; + if (nextKey !== initKey) { + initKey = nextKey; + resetForm(); } }); @@ -150,7 +161,7 @@ } - + {edit ? 'Редактировать источник' : 'Новый CDN-источник'} diff --git a/web/src/lib/components/modules/ModuleCdnSourcesCard.svelte b/web/src/lib/components/modules/ModuleCdnSourcesCard.svelte index 0ec8e02..5664935 100644 --- a/web/src/lib/components/modules/ModuleCdnSourcesCard.svelte +++ b/web/src/lib/components/modules/ModuleCdnSourcesCard.svelte @@ -39,8 +39,12 @@ let selectedIds = $state(new Set()); let deletingBulk = $state(false); - const selectedCount = $derived(selectedIds.size); - const allSelected = $derived(sources.length > 0 && selectedIds.size === sources.length); + const activeSelected = $derived.by(() => { + const allowed = new Set(sources.map((s) => s.id)); + return [...selectedIds].filter((id) => allowed.has(id)); + }); + const selectedCount = $derived(activeSelected.length); + const allSelected = $derived(sources.length > 0 && sources.every((s) => selectedIds.has(s.id))); const columns = [ { id: 'select', label: '', class: 'w-10' }, @@ -62,11 +66,6 @@ { id: 'actions', label: '', class: 'w-20' } ] as const; - $effect(() => { - const validIds = new Set(sources.map((s) => s.id)); - selectedIds = new Set([...selectedIds].filter((id) => validIds.has(id))); - }); - function toggleSelection(id: string) { const next = new Set(selectedIds); if (next.has(id)) next.delete(id); @@ -120,7 +119,7 @@ deletingBulk = true; let deleted = 0; try { - for (const id of selectedIds) { + for (const id of activeSelected) { try { await apiMutate(`/v1/modules/${moduleId}/cdn-sources/${id}`, 'DELETE', undefined, { idempotent: false diff --git a/web/src/lib/components/modules/ModuleCreateDialog.svelte b/web/src/lib/components/modules/ModuleCreateDialog.svelte index 0cde457..5d97705 100644 --- a/web/src/lib/components/modules/ModuleCreateDialog.svelte +++ b/web/src/lib/components/modules/ModuleCreateDialog.svelte @@ -72,7 +72,7 @@ } - + Новый модуль diff --git a/web/src/lib/components/modules/ModuleDetailHeader.svelte b/web/src/lib/components/modules/ModuleDetailHeader.svelte index 636544e..29f8c0f 100644 --- a/web/src/lib/components/modules/ModuleDetailHeader.svelte +++ b/web/src/lib/components/modules/ModuleDetailHeader.svelte @@ -39,15 +39,15 @@ {moduleEnabledRu(!!mod.enabled)} - - - diff --git a/web/src/lib/components/modules/ModuleDomainEntriesCard.svelte b/web/src/lib/components/modules/ModuleDomainEntriesCard.svelte index 88b8783..f8b7853 100644 --- a/web/src/lib/components/modules/ModuleDomainEntriesCard.svelte +++ b/web/src/lib/components/modules/ModuleDomainEntriesCard.svelte @@ -44,8 +44,12 @@ let csvExporting = $state(false); let csvFileInput = $state(null); - const selectedCount = $derived(selectedIds.size); - const allSelected = $derived(entries.length > 0 && selectedIds.size === entries.length); + const activeSelected = $derived.by(() => { + const allowed = new Set(entries.map((e) => e.id)); + return [...selectedIds].filter((id) => allowed.has(id)); + }); + const selectedCount = $derived(activeSelected.length); + const allSelected = $derived(entries.length > 0 && entries.every((e) => selectedIds.has(e.id))); const columns = [ { id: 'select', label: '', class: 'w-10' }, @@ -54,11 +58,6 @@ { id: 'actions', label: '', class: 'w-20' } ] as const; - $effect(() => { - const validIds = new Set(entries.map((e) => e.id)); - selectedIds = new Set([...selectedIds].filter((id) => validIds.has(id))); - }); - function toggleSelection(id: string) { const next = new Set(selectedIds); if (next.has(id)) next.delete(id); @@ -112,7 +111,7 @@ deletingBulk = true; let deleted = 0; try { - for (const id of selectedIds) { + for (const id of activeSelected) { try { await apiMutate(`/v1/modules/${moduleId}/domain-entries/${id}`, 'DELETE', undefined, { idempotent: false diff --git a/web/src/lib/components/modules/ModuleDomainEntryDialog.svelte b/web/src/lib/components/modules/ModuleDomainEntryDialog.svelte index 5aca8b3..8b7ad28 100644 --- a/web/src/lib/components/modules/ModuleDomainEntryDialog.svelte +++ b/web/src/lib/components/modules/ModuleDomainEntryDialog.svelte @@ -34,12 +34,23 @@ let saving = $state(false); let form = $state({ fqdn: '', community_id: null }); + let initKey = $state(''); + + function resetForm() { + form = edit + ? { fqdn: edit.fqdn, community_id: edit.community_id } + : { fqdn: '', community_id: null }; + } $effect(() => { - if (open) { - form = edit - ? { fqdn: edit.fqdn, community_id: edit.community_id } - : { fqdn: '', community_id: null }; + if (!open) { + initKey = ''; + return; + } + const nextKey = edit?.id ?? 'new'; + if (nextKey !== initKey) { + initKey = nextKey; + resetForm(); } }); @@ -68,7 +79,7 @@ } - + {edit ? 'Редактировать домен' : 'Новый домен'} diff --git a/web/src/lib/components/modules/ModuleEditDialog.svelte b/web/src/lib/components/modules/ModuleEditDialog.svelte index 9d6e75f..44a62a5 100644 --- a/web/src/lib/components/modules/ModuleEditDialog.svelte +++ b/web/src/lib/components/modules/ModuleEditDialog.svelte @@ -71,19 +71,30 @@ let editForm = $state({}); let editSaving = $state(false); + let initKey = $state(''); + + function resetEditForm() { + editForm = { + name: mod.name, + enabled: mod.enabled, + priority: mod.priority, + refresh_interval_sec: mod.refresh_interval_sec, + cron_expr: mod.cron_expr, + default_community_id: mod.default_community_id, + doh_profile_ids: moduleDohProfileIds(mod), + doh_resolver_policy: mod.doh_resolver_policy ?? 'primary_only' + }; + } $effect(() => { - if (open && mod) { - editForm = { - name: mod.name, - enabled: mod.enabled, - priority: mod.priority, - refresh_interval_sec: mod.refresh_interval_sec, - cron_expr: mod.cron_expr, - default_community_id: mod.default_community_id, - doh_profile_ids: moduleDohProfileIds(mod), - doh_resolver_policy: mod.doh_resolver_policy ?? 'primary_only' - }; + if (!open) { + initKey = ''; + return; + } + const nextKey = mod.id; + if (nextKey !== initKey) { + initKey = nextKey; + resetEditForm(); } }); @@ -136,7 +147,7 @@ } - + Редактировать модуль diff --git a/web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte b/web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte index 135f958..bf54d79 100644 --- a/web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte +++ b/web/src/lib/components/modules/ModuleIpRangeEntryDialog.svelte @@ -28,12 +28,23 @@ let saving = $state(false); let form = $state({ prefix: '', community_id: '' }); + let initKey = $state(''); + + function resetForm() { + form = edit + ? { prefix: edit.prefix, community_id: edit.community_id } + : { prefix: '', community_id: '' }; + } $effect(() => { - if (open) { - form = edit - ? { prefix: edit.prefix, community_id: edit.community_id } - : { prefix: '', community_id: '' }; + if (!open) { + initKey = ''; + return; + } + const nextKey = edit?.id ?? 'new'; + if (nextKey !== initKey) { + initKey = nextKey; + resetForm(); } }); @@ -62,7 +73,7 @@ } - + {edit ? 'Редактировать диапазон' : 'Новый IP-диапазон'} diff --git a/web/src/lib/components/modules/ModuleIpRangesCard.svelte b/web/src/lib/components/modules/ModuleIpRangesCard.svelte index acb1b02..3e9ac94 100644 --- a/web/src/lib/components/modules/ModuleIpRangesCard.svelte +++ b/web/src/lib/components/modules/ModuleIpRangesCard.svelte @@ -44,8 +44,12 @@ let csvExporting = $state(false); let csvFileInput = $state(null); - const selectedCount = $derived(selectedIds.size); - const allSelected = $derived(entries.length > 0 && selectedIds.size === entries.length); + const activeSelected = $derived.by(() => { + const allowed = new Set(entries.map((e) => e.id)); + return [...selectedIds].filter((id) => allowed.has(id)); + }); + const selectedCount = $derived(activeSelected.length); + const allSelected = $derived(entries.length > 0 && entries.every((e) => selectedIds.has(e.id))); const columns = [ { id: 'select', label: '', class: 'w-10' }, @@ -59,11 +63,6 @@ { id: 'actions', label: '', class: 'w-20' } ] as const; - $effect(() => { - const validIds = new Set(entries.map((e) => e.id)); - selectedIds = new Set([...selectedIds].filter((id) => validIds.has(id))); - }); - function toggleSelection(id: string) { const next = new Set(selectedIds); if (next.has(id)) next.delete(id); @@ -120,7 +119,7 @@ deletingBulk = true; let deleted = 0; try { - for (const id of selectedIds) { + for (const id of activeSelected) { try { await apiMutate(`/v1/modules/${moduleId}/ip-range-entries/${id}`, 'DELETE', undefined, { idempotent: false diff --git a/web/src/lib/ui/patterns/confirm/confirm-dialog.svelte b/web/src/lib/ui/patterns/confirm/confirm-dialog.svelte index 9db74bc..3935180 100644 --- a/web/src/lib/ui/patterns/confirm/confirm-dialog.svelte +++ b/web/src/lib/ui/patterns/confirm/confirm-dialog.svelte @@ -15,7 +15,12 @@ const open = $derived(!!state?.open); function onOpenChange(v: boolean) { - if (!v) closeConfirm(); + if (!v && state && !state.loading) closeConfirm(); + } + + async function handleConfirm() { + if (!state || state.loading) return; + await state.onConfirm(); } @@ -37,7 +42,10 @@ ? 'text-destructive-foreground bg-destructive hover:bg-destructive/90' : ''} disabled={state.loading} - onclick={state.onConfirm} + onclick={(e) => { + e.preventDefault(); + void handleConfirm(); + }} > {state.loading ? '…' : (state.confirmLabel ?? 'Подтвердить')} diff --git a/web/src/lib/ui/patterns/confirm/confirm-state.svelte.ts b/web/src/lib/ui/patterns/confirm/confirm-state.svelte.ts index d30bd20..44b0784 100644 --- a/web/src/lib/ui/patterns/confirm/confirm-state.svelte.ts +++ b/web/src/lib/ui/patterns/confirm/confirm-state.svelte.ts @@ -21,8 +21,9 @@ export function confirm(options: ConfirmOptions) { open: true, loading: false, onConfirm: async () => { - if (!confirmState.current) return; - confirmState.current = { ...confirmState.current, loading: true }; + const current = confirmState.current; + if (!current || current.loading) return; + confirmState.current = { ...current, loading: true }; try { await options.onConfirm(); resolve(true);