Files
nxdns/admin/src/ui/useCrudForm.ts
T

85 lines
2.6 KiB
TypeScript

import { useState } from "react";
import { useMutation, useQueryClient, type QueryClient, type UseMutationOptions } from "@tanstack/react-query";
/** Which form is open: none, the create form, or the edit form for one row. */
export type CrudFormState<Entity> = { mode: "create" } | { mode: "edit"; entity: Entity };
/** The row a confirmation dialog is open for, and the message it shows. */
export interface PendingDelete<Entity> {
entity: Entity;
message: string;
}
type MutationFactory<Variables> = (queryClient: QueryClient) => UseMutationOptions<unknown, Error, Variables>;
export interface CrudFormSpec<Entity extends { id: number }, Input> {
create: MutationFactory<Input>;
update: MutationFactory<{ id: number; input: Input }>;
remove: MutationFactory<number>;
/** The message the confirmation dialog shows before a delete. */
confirmDelete: (entity: Entity) => string;
}
/**
* The create/edit/delete plumbing shared by the local DNS tabs (ruling 10):
* three mutations, the open-form state, the pending-delete state and the
* handlers. The field JSX, the table and the confirmation dialog stay in the
* caller: `onDelete` opens the confirmation instead of deciding anything, so
* the hook never reaches for `window.confirm` (ruling 5).
*/
export function useCrudForm<Entity extends { id: number }, Input>(spec: CrudFormSpec<Entity, Input>) {
const queryClient = useQueryClient();
const create = useMutation(spec.create(queryClient));
const update = useMutation(spec.update(queryClient));
const remove = useMutation(spec.remove(queryClient));
const [form, setForm] = useState<CrudFormState<Entity> | null>(null);
const [pendingDelete, setPendingDelete] = useState<PendingDelete<Entity> | null>(null);
function openForm(next: CrudFormState<Entity>) {
create.reset();
update.reset();
setForm(next);
}
function closeForm() {
setForm(null);
}
function onSubmit(input: Input) {
if (form === null) return;
if (form.mode === "create") {
create.mutate(input, { onSuccess: () => setForm(null) });
} else {
update.mutate({ id: form.entity.id, input }, { onSuccess: () => setForm(null) });
}
}
function onDelete(entity: Entity) {
setPendingDelete({ entity, message: spec.confirmDelete(entity) });
}
function confirmPendingDelete() {
if (pendingDelete === null) return;
remove.mutate(pendingDelete.entity.id);
setPendingDelete(null);
}
function cancelPendingDelete() {
setPendingDelete(null);
}
return {
create,
update,
remove,
form,
openForm,
closeForm,
onSubmit,
onDelete,
pendingDelete,
confirmPendingDelete,
cancelPendingDelete,
};
}