Compare commits

..
1 Commits
Author SHA1 Message Date
Denozordec 8204105fd6 feat(web): enhance module entry dialogs and selection handling
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 38s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m1s
- Refactored module entry dialogs to reset forms based on edit state and improve state management.
- Updated selection logic in various components to utilize derived states for active selections, enhancing bulk operations.
- Improved dialog bindings for better state synchronization and user experience.
- Streamlined component structure for maintainability and clarity.
2026-05-20 12:35:45 +07:00
13 changed files with 140 additions and 79 deletions
@@ -46,8 +46,13 @@
let csvExporting = $state(false); let csvExporting = $state(false);
let csvFileInput = $state<HTMLInputElement | null>(null); let csvFileInput = $state<HTMLInputElement | null>(null);
const selectedCount = $derived(selectedIds.size); const activeSelected = $derived.by(() => {
const allSelected = $derived(entries.length > 0 && selectedIds.size === entries.length); 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 = [ const columns = [
{ id: 'select', label: '', class: 'w-10' }, { id: 'select', label: '', class: 'w-10' },
@@ -75,11 +80,6 @@
{ id: 'actions', label: '', class: 'w-20' } { id: 'actions', label: '', class: 'w-20' }
] as const; ] 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) { function toggleSelection(id: string) {
const next = new Set(selectedIds); const next = new Set(selectedIds);
if (next.has(id)) next.delete(id); if (next.has(id)) next.delete(id);
@@ -133,7 +133,7 @@
deletingBulk = true; deletingBulk = true;
let deleted = 0; let deleted = 0;
try { try {
for (const id of selectedIds) { for (const id of activeSelected) {
try { try {
await apiMutate(`/v1/modules/${moduleId}/as-entries/${id}`, 'DELETE', undefined, { await apiMutate(`/v1/modules/${moduleId}/as-entries/${id}`, 'DELETE', undefined, {
idempotent: false idempotent: false
@@ -35,13 +35,24 @@
let saving = $state(false); let saving = $state(false);
let form = $state<AsEntryCreate>({ asn: 0, community_id: null }); let form = $state<AsEntryCreate>({ asn: 0, community_id: null });
let initKey = $state('');
$effect(() => { function resetForm() {
if (open) {
form = edit form = edit
? { asn: edit.asn, community_id: edit.community_id } ? { asn: edit.asn, community_id: edit.community_id }
: { asn: 0, community_id: null }; : { asn: 0, community_id: null };
} }
$effect(() => {
if (!open) {
initKey = '';
return;
}
const nextKey = edit?.id ?? 'new';
if (nextKey !== initKey) {
initKey = nextKey;
resetForm();
}
}); });
async function save() { async function save() {
@@ -75,7 +86,7 @@
} }
</script> </script>
<Dialog {open} onOpenChange={handleOpenChange}> <Dialog bind:open onOpenChange={handleOpenChange}>
<DialogContent class="sm:max-w-sm"> <DialogContent class="sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle>{edit ? 'Редактировать запись' : 'Новая AS-запись'}</DialogTitle> <DialogTitle>{edit ? 'Редактировать запись' : 'Новая AS-запись'}</DialogTitle>
@@ -51,6 +51,7 @@
prefix_path: '', prefix_path: '',
community_id: null community_id: null
}); });
let initKey = $state('');
function clearPreview() { function clearPreview() {
previewLoading = false; previewLoading = false;
@@ -61,8 +62,7 @@
previewOk = false; previewOk = false;
} }
$effect(() => { function resetForm() {
if (open) {
clearPreview(); clearPreview();
form = edit form = edit
? { ? {
@@ -74,6 +74,17 @@
} }
: { url: '', source_kind: 'plaintext', prefix_path: '', community_id: null }; : { url: '', source_kind: 'plaintext', prefix_path: '', community_id: null };
} }
$effect(() => {
if (!open) {
initKey = '';
return;
}
const nextKey = edit?.id ?? 'new';
if (nextKey !== initKey) {
initKey = nextKey;
resetForm();
}
}); });
async function previewCdn() { async function previewCdn() {
@@ -150,7 +161,7 @@
} }
</script> </script>
<Dialog {open} onOpenChange={handleOpenChange}> <Dialog bind:open onOpenChange={handleOpenChange}>
<DialogContent class="sm:max-w-lg"> <DialogContent class="sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>{edit ? 'Редактировать источник' : 'Новый CDN-источник'}</DialogTitle> <DialogTitle>{edit ? 'Редактировать источник' : 'Новый CDN-источник'}</DialogTitle>
@@ -39,8 +39,12 @@
let selectedIds = $state(new Set<string>()); let selectedIds = $state(new Set<string>());
let deletingBulk = $state(false); let deletingBulk = $state(false);
const selectedCount = $derived(selectedIds.size); const activeSelected = $derived.by(() => {
const allSelected = $derived(sources.length > 0 && selectedIds.size === sources.length); 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 = [ const columns = [
{ id: 'select', label: '', class: 'w-10' }, { id: 'select', label: '', class: 'w-10' },
@@ -62,11 +66,6 @@
{ id: 'actions', label: '', class: 'w-20' } { id: 'actions', label: '', class: 'w-20' }
] as const; ] 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) { function toggleSelection(id: string) {
const next = new Set(selectedIds); const next = new Set(selectedIds);
if (next.has(id)) next.delete(id); if (next.has(id)) next.delete(id);
@@ -120,7 +119,7 @@
deletingBulk = true; deletingBulk = true;
let deleted = 0; let deleted = 0;
try { try {
for (const id of selectedIds) { for (const id of activeSelected) {
try { try {
await apiMutate(`/v1/modules/${moduleId}/cdn-sources/${id}`, 'DELETE', undefined, { await apiMutate(`/v1/modules/${moduleId}/cdn-sources/${id}`, 'DELETE', undefined, {
idempotent: false idempotent: false
@@ -72,7 +72,7 @@
} }
</script> </script>
<Dialog {open} onOpenChange={handleOpenChange}> <Dialog bind:open onOpenChange={handleOpenChange}>
<DialogContent class="sm:max-w-md"> <DialogContent class="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Новый модуль</DialogTitle> <DialogTitle>Новый модуль</DialogTitle>
@@ -39,15 +39,15 @@
<Badge variant={moduleEnabledBadgeVariant(!!mod.enabled)} class="text-xs"> <Badge variant={moduleEnabledBadgeVariant(!!mod.enabled)} class="text-xs">
{moduleEnabledRu(!!mod.enabled)} {moduleEnabledRu(!!mod.enabled)}
</Badge> </Badge>
<Button variant="outline" size="sm" onclick={onRefresh} disabled={refreshing}> <Button variant="outline" size="sm" onclick={() => onRefresh()} disabled={refreshing}>
<RefreshCw class={refreshing ? 'animate-spin' : ''} /> <RefreshCw class={refreshing ? 'animate-spin' : ''} />
Обновить Обновить
</Button> </Button>
<Button variant="outline" size="sm" onclick={onEdit}> <Button variant="outline" size="sm" onclick={() => onEdit()}>
<Pencil /> <Pencil />
Редактировать Редактировать
</Button> </Button>
<Button variant="destructive" size="sm" onclick={onDelete}> <Button variant="destructive" size="sm" onclick={() => onDelete()}>
<Trash2 /> <Trash2 />
Удалить Удалить
</Button> </Button>
@@ -44,8 +44,12 @@
let csvExporting = $state(false); let csvExporting = $state(false);
let csvFileInput = $state<HTMLInputElement | null>(null); let csvFileInput = $state<HTMLInputElement | null>(null);
const selectedCount = $derived(selectedIds.size); const activeSelected = $derived.by(() => {
const allSelected = $derived(entries.length > 0 && selectedIds.size === entries.length); 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 = [ const columns = [
{ id: 'select', label: '', class: 'w-10' }, { id: 'select', label: '', class: 'w-10' },
@@ -54,11 +58,6 @@
{ id: 'actions', label: '', class: 'w-20' } { id: 'actions', label: '', class: 'w-20' }
] as const; ] 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) { function toggleSelection(id: string) {
const next = new Set(selectedIds); const next = new Set(selectedIds);
if (next.has(id)) next.delete(id); if (next.has(id)) next.delete(id);
@@ -112,7 +111,7 @@
deletingBulk = true; deletingBulk = true;
let deleted = 0; let deleted = 0;
try { try {
for (const id of selectedIds) { for (const id of activeSelected) {
try { try {
await apiMutate(`/v1/modules/${moduleId}/domain-entries/${id}`, 'DELETE', undefined, { await apiMutate(`/v1/modules/${moduleId}/domain-entries/${id}`, 'DELETE', undefined, {
idempotent: false idempotent: false
@@ -34,13 +34,24 @@
let saving = $state(false); let saving = $state(false);
let form = $state<DomainEntryCreate>({ fqdn: '', community_id: null }); let form = $state<DomainEntryCreate>({ fqdn: '', community_id: null });
let initKey = $state('');
$effect(() => { function resetForm() {
if (open) {
form = edit form = edit
? { fqdn: edit.fqdn, community_id: edit.community_id } ? { fqdn: edit.fqdn, community_id: edit.community_id }
: { fqdn: '', community_id: null }; : { fqdn: '', community_id: null };
} }
$effect(() => {
if (!open) {
initKey = '';
return;
}
const nextKey = edit?.id ?? 'new';
if (nextKey !== initKey) {
initKey = nextKey;
resetForm();
}
}); });
async function save() { async function save() {
@@ -68,7 +79,7 @@
} }
</script> </script>
<Dialog {open} onOpenChange={handleOpenChange}> <Dialog bind:open onOpenChange={handleOpenChange}>
<DialogContent class="sm:max-w-sm"> <DialogContent class="sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle>{edit ? 'Редактировать домен' : 'Новый домен'}</DialogTitle> <DialogTitle>{edit ? 'Редактировать домен' : 'Новый домен'}</DialogTitle>
@@ -71,9 +71,9 @@
let editForm = $state<ModulePatch>({}); let editForm = $state<ModulePatch>({});
let editSaving = $state(false); let editSaving = $state(false);
let initKey = $state('');
$effect(() => { function resetEditForm() {
if (open && mod) {
editForm = { editForm = {
name: mod.name, name: mod.name,
enabled: mod.enabled, enabled: mod.enabled,
@@ -85,6 +85,17 @@
doh_resolver_policy: mod.doh_resolver_policy ?? 'primary_only' doh_resolver_policy: mod.doh_resolver_policy ?? 'primary_only'
}; };
} }
$effect(() => {
if (!open) {
initKey = '';
return;
}
const nextKey = mod.id;
if (nextKey !== initKey) {
initKey = nextKey;
resetEditForm();
}
}); });
function toggleEditDohProfile(id: string, checked: boolean) { function toggleEditDohProfile(id: string, checked: boolean) {
@@ -136,7 +147,7 @@
} }
</script> </script>
<Dialog {open} onOpenChange={handleOpenChange}> <Dialog bind:open onOpenChange={handleOpenChange}>
<DialogContent class="sm:max-w-md"> <DialogContent class="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Редактировать модуль</DialogTitle> <DialogTitle>Редактировать модуль</DialogTitle>
@@ -28,13 +28,24 @@
let saving = $state(false); let saving = $state(false);
let form = $state<IpRangeEntryCreate>({ prefix: '', community_id: '' }); let form = $state<IpRangeEntryCreate>({ prefix: '', community_id: '' });
let initKey = $state('');
$effect(() => { function resetForm() {
if (open) {
form = edit form = edit
? { prefix: edit.prefix, community_id: edit.community_id } ? { prefix: edit.prefix, community_id: edit.community_id }
: { prefix: '', community_id: '' }; : { prefix: '', community_id: '' };
} }
$effect(() => {
if (!open) {
initKey = '';
return;
}
const nextKey = edit?.id ?? 'new';
if (nextKey !== initKey) {
initKey = nextKey;
resetForm();
}
}); });
async function save() { async function save() {
@@ -62,7 +73,7 @@
} }
</script> </script>
<Dialog {open} onOpenChange={handleOpenChange}> <Dialog bind:open onOpenChange={handleOpenChange}>
<DialogContent class="sm:max-w-sm"> <DialogContent class="sm:max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle>{edit ? 'Редактировать диапазон' : 'Новый IP-диапазон'}</DialogTitle> <DialogTitle>{edit ? 'Редактировать диапазон' : 'Новый IP-диапазон'}</DialogTitle>
@@ -44,8 +44,12 @@
let csvExporting = $state(false); let csvExporting = $state(false);
let csvFileInput = $state<HTMLInputElement | null>(null); let csvFileInput = $state<HTMLInputElement | null>(null);
const selectedCount = $derived(selectedIds.size); const activeSelected = $derived.by(() => {
const allSelected = $derived(entries.length > 0 && selectedIds.size === entries.length); 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 = [ const columns = [
{ id: 'select', label: '', class: 'w-10' }, { id: 'select', label: '', class: 'w-10' },
@@ -59,11 +63,6 @@
{ id: 'actions', label: '', class: 'w-20' } { id: 'actions', label: '', class: 'w-20' }
] as const; ] 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) { function toggleSelection(id: string) {
const next = new Set(selectedIds); const next = new Set(selectedIds);
if (next.has(id)) next.delete(id); if (next.has(id)) next.delete(id);
@@ -120,7 +119,7 @@
deletingBulk = true; deletingBulk = true;
let deleted = 0; let deleted = 0;
try { try {
for (const id of selectedIds) { for (const id of activeSelected) {
try { try {
await apiMutate(`/v1/modules/${moduleId}/ip-range-entries/${id}`, 'DELETE', undefined, { await apiMutate(`/v1/modules/${moduleId}/ip-range-entries/${id}`, 'DELETE', undefined, {
idempotent: false idempotent: false
@@ -15,7 +15,12 @@
const open = $derived(!!state?.open); const open = $derived(!!state?.open);
function onOpenChange(v: boolean) { function onOpenChange(v: boolean) {
if (!v) closeConfirm(); if (!v && state && !state.loading) closeConfirm();
}
async function handleConfirm() {
if (!state || state.loading) return;
await state.onConfirm();
} }
</script> </script>
@@ -37,7 +42,10 @@
? 'text-destructive-foreground bg-destructive hover:bg-destructive/90' ? 'text-destructive-foreground bg-destructive hover:bg-destructive/90'
: ''} : ''}
disabled={state.loading} disabled={state.loading}
onclick={state.onConfirm} onclick={(e) => {
e.preventDefault();
void handleConfirm();
}}
> >
{state.loading ? '…' : (state.confirmLabel ?? 'Подтвердить')} {state.loading ? '…' : (state.confirmLabel ?? 'Подтвердить')}
</AlertDialogAction> </AlertDialogAction>
@@ -21,8 +21,9 @@ export function confirm(options: ConfirmOptions) {
open: true, open: true,
loading: false, loading: false,
onConfirm: async () => { onConfirm: async () => {
if (!confirmState.current) return; const current = confirmState.current;
confirmState.current = { ...confirmState.current, loading: true }; if (!current || current.loading) return;
confirmState.current = { ...current, loading: true };
try { try {
await options.onConfirm(); await options.onConfirm();
resolve(true); resolve(true);