fix: reject snapshot when template already attached (409); handle domains-load error; drop orphaned useDeleteDomain

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 12:54:52 +07:00
parent 137113cbe6
commit 9f0938daea
5 changed files with 80 additions and 17 deletions
-11
View File
@@ -111,17 +111,6 @@ export function useSetDomainTemplate() {
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains", project?.id] }),
})
}
export function useDeleteDomain() {
const { project } = useAuth()
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) => {
const pid = requireProjectId(project)
return api.deleteDomain(pid, id)
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains", project?.id] }),
})
}
export function useCheckDomain(id: string, enabled = true) {
const { project } = useAuth()
return useQuery({
+17 -2
View File
@@ -19,8 +19,7 @@ const domainWithTemplate: Domain = {
lastCheckStatus: "drift",
}
function renderPage() {
const qc = new QueryClient()
function renderPage(qc: QueryClient = new QueryClient()) {
return render(
<QueryClientProvider client={qc}>
<AuthProvider>
@@ -111,6 +110,22 @@ test("домен без шаблона показывает записи зон
expect(checkSpy).not.toHaveBeenCalled()
})
test("ошибка загрузки списка доменов показывает баннер ошибки и не уходит в ветку без шаблона", async () => {
vi.spyOn(api, "listDomains").mockRejectedValue(new Error("network down"))
const checkSpy = vi.spyOn(api, "checkDomain")
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
// retry:false — иначе react-query переретраит listDomains с экспоненциальной
// задержкой и findByText не успевает дождаться финального isError.
renderPage(new QueryClient({ defaultOptions: { queries: { retry: false } } }))
expect(await screen.findByText(/не удалось загрузить список доменов/i)).toBeInTheDocument()
expect(screen.getByText("network down")).toBeInTheDocument()
expect(screen.queryByText(/шаблон не привязан/i)).not.toBeInTheDocument()
expect(checkSpy).not.toHaveBeenCalled()
expect(zoneRecordsSpy).not.toHaveBeenCalled()
})
test("создание шаблона из зоны вызывает templateFromZone", async () => {
vi.spyOn(api, "listDomains").mockResolvedValue([
{ id: "d1", providerAccountId: "acc1", zoneName: "example.com.", zoneId: "z1", templateId: null },
+24 -4
View File
@@ -31,10 +31,11 @@ export function DomainDiffPage() {
const check = useCheckDomain(id, hasTemplate)
const apply = useApplyDomain(id)
// Пока список доменов не загружен, hasTemplate недостоверно (false по
// умолчанию из-за domain === undefined) — не дёргаем provider-запрос
// записей зоны, пока не будет точно известно, что шаблона нет.
const zoneRecords = useZoneRecords(id, !domains.isPending && !hasTemplate)
// Пока список доменов не загружен ИЛИ загрузка упала ошибкой, hasTemplate
// недостоверно (false по умолчанию из-за domain === undefined) — не
// дёргаем provider-запрос записей зоны, пока не будет точно известно
// (успешный ответ), что шаблона нет.
const zoneRecords = useZoneRecords(id, !domains.isPending && !domains.isError && !hasTemplate)
const createTemplateFromZone = useCreateTemplateFromZone()
const [applyPrunes, setApplyPrunes] = useState(false)
const pruneCheckboxId = useId()
@@ -64,6 +65,25 @@ export function DomainDiffPage() {
)
}
// Список доменов не загрузился — hasTemplate тут недостоверно (domain
// === undefined из-за domains.data === undefined даёт hasTemplate=false),
// поэтому без этой проверки страница молча уходит в ветку «без шаблона»
// и дёргает zoneRecords для несуществующего состояния. Показываем ошибку
// и не рендерим ни одну из веток решения.
if (domains.isError) {
return (
<div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-8">
<div className="flex items-start gap-2.5 rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
<AlertTriangle className="mt-0.5 size-4 shrink-0" strokeWidth={1.75} />
<div className="flex flex-col gap-1">
<span className="font-medium">Не удалось загрузить список доменов</span>
<span className="font-dns text-xs opacity-90">{domains.error.message}</span>
</div>
</div>
</div>
)
}
return (
<div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-8">
<header className="flex flex-wrap items-end justify-between gap-4">