85 lines
2.6 KiB
TypeScript
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,
|
|
};
|
|
}
|