milestone 18: collapse duplicated infrastructure into shared listener core, crud list helper, resource shells, transport race, name and line helpers, ui modules
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useId, useState, type FormEvent } from "react";
|
||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
forwardZoneCreateMutation,
|
||||
forwardZoneDeleteMutation,
|
||||
@@ -8,17 +8,15 @@ import {
|
||||
} from "@/lib/queries";
|
||||
import type { ForwardZone, ForwardZoneInput } from "@/lib/types";
|
||||
import InlineError from "@/lib/InlineError";
|
||||
|
||||
const inputClass =
|
||||
"mt-1 w-full rounded border border-zinc-300 bg-white px-3 py-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700 dark:bg-zinc-900";
|
||||
const primaryButtonClass =
|
||||
"rounded bg-blue-600 px-3 py-2 font-medium text-white disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600";
|
||||
const secondaryButtonClass =
|
||||
"rounded border border-zinc-300 px-3 py-2 font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700";
|
||||
const rowButtonClass =
|
||||
"rounded px-2 py-1 text-sm text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:text-blue-400";
|
||||
|
||||
type FormState = { mode: "create" } | { mode: "edit"; zone: ForwardZone };
|
||||
import { useCrudForm } from "@/ui/useCrudForm";
|
||||
import {
|
||||
formCardClass,
|
||||
inputClass,
|
||||
largeButtonClass,
|
||||
largePrimaryButtonClass,
|
||||
rowButtonClass,
|
||||
tableWrapClass,
|
||||
} from "@/ui/classes";
|
||||
|
||||
function ZoneForm({
|
||||
initial,
|
||||
@@ -43,10 +41,7 @@ function ZoneForm({
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="mt-4 max-w-lg space-y-3 rounded border border-zinc-200 p-4 dark:border-zinc-800"
|
||||
>
|
||||
<form onSubmit={submit} className={formCardClass}>
|
||||
<h3 className="font-medium">{initial === undefined ? "New forward zone" : `Edit ${initial.zone}`}</h3>
|
||||
<div>
|
||||
<label htmlFor={`${id}-zone`} className="block text-sm font-medium">
|
||||
@@ -75,10 +70,10 @@ function ZoneForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={busy} className={primaryButtonClass}>
|
||||
<button type="submit" disabled={busy} className={largePrimaryButtonClass}>
|
||||
{busy ? "Saving…" : "Save"}
|
||||
</button>
|
||||
<button type="button" onClick={onCancel} className={secondaryButtonClass}>
|
||||
<button type="button" onClick={onCancel} className={largeButtonClass}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -89,31 +84,15 @@ function ZoneForm({
|
||||
|
||||
export default function ZonesTab() {
|
||||
const zones = useSuspenseQuery(forwardZonesQuery()).data;
|
||||
const queryClient = useQueryClient();
|
||||
const create = useMutation(forwardZoneCreateMutation(queryClient));
|
||||
const update = useMutation(forwardZoneUpdateMutation(queryClient));
|
||||
const remove = useMutation(forwardZoneDeleteMutation(queryClient));
|
||||
const [form, setForm] = useState<FormState | null>(null);
|
||||
|
||||
function openForm(next: FormState) {
|
||||
create.reset();
|
||||
update.reset();
|
||||
setForm(next);
|
||||
}
|
||||
|
||||
function onSubmit(input: ForwardZoneInput) {
|
||||
if (form === null) return;
|
||||
if (form.mode === "create") {
|
||||
create.mutate(input, { onSuccess: () => setForm(null) });
|
||||
} else {
|
||||
update.mutate({ id: form.zone.id, input }, { onSuccess: () => setForm(null) });
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete(zone: ForwardZone) {
|
||||
if (!window.confirm(`Delete forward zone "${zone.zone}"?`)) return;
|
||||
remove.mutate(zone.id);
|
||||
}
|
||||
const { create, update, remove, form, openForm, closeForm, onSubmit, onDelete } = useCrudForm<
|
||||
ForwardZone,
|
||||
ForwardZoneInput
|
||||
>({
|
||||
create: forwardZoneCreateMutation,
|
||||
update: forwardZoneUpdateMutation,
|
||||
remove: forwardZoneDeleteMutation,
|
||||
confirmDelete: (zone) => `Delete forward zone "${zone.zone}"?`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -121,20 +100,15 @@ export default function ZonesTab() {
|
||||
<p className="text-sm text-zinc-500">
|
||||
Names under these zones go to their own resolver. Changes apply live.
|
||||
</p>
|
||||
<button type="button" onClick={() => openForm({ mode: "create" })} className={primaryButtonClass}>
|
||||
<button type="button" onClick={() => openForm({ mode: "create" })} className={largePrimaryButtonClass}>
|
||||
Add zone
|
||||
</button>
|
||||
</div>
|
||||
<InlineError error={remove.error} />
|
||||
{form?.mode === "create" && (
|
||||
<ZoneForm
|
||||
busy={create.isPending}
|
||||
error={create.error}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => setForm(null)}
|
||||
/>
|
||||
<ZoneForm busy={create.isPending} error={create.error} onSubmit={onSubmit} onCancel={closeForm} />
|
||||
)}
|
||||
<div className="mt-4 overflow-x-auto">
|
||||
<div className={tableWrapClass}>
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 text-zinc-500 dark:border-zinc-800">
|
||||
@@ -159,14 +133,14 @@ export default function ZonesTab() {
|
||||
)}
|
||||
{zones.map((zone) => (
|
||||
<tr key={zone.id} className="border-b border-zinc-100 dark:border-zinc-900">
|
||||
{form?.mode === "edit" && form.zone.id === zone.id ? (
|
||||
{form?.mode === "edit" && form.entity.id === zone.id ? (
|
||||
<td colSpan={3}>
|
||||
<ZoneForm
|
||||
initial={zone}
|
||||
busy={update.isPending}
|
||||
error={update.error}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => setForm(null)}
|
||||
onCancel={closeForm}
|
||||
/>
|
||||
</td>
|
||||
) : (
|
||||
@@ -176,7 +150,7 @@ export default function ZonesTab() {
|
||||
<td className="py-2 text-right whitespace-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openForm({ mode: "edit", zone })}
|
||||
onClick={() => openForm({ mode: "edit", entity: zone })}
|
||||
className={rowButtonClass}
|
||||
>
|
||||
Edit
|
||||
|
||||
Reference in New Issue
Block a user