admin/lib/formValidationToast.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

61 lines
1.7 KiB
TypeScript

import type { FieldErrors, FieldValues } from 'react-hook-form'
import { texts } from '@/texts'
import { addToast } from '@/lib/toast'
function isPlainObject(value: object): boolean {
const proto: object | null = Object.getPrototypeOf(value) as object | null
return proto === Object.prototype || proto === null
}
/**
* Walk RHF `FieldErrors` for the first non-empty `message`.
* Skips `ref` (DOM / focus handles — often circular) and non-plain objects
* so a validation toast can never blow the call stack.
*/
function findFirstErrorMessage(value: unknown, seen: WeakSet<object> = new WeakSet<object>()): string | undefined {
if (!value || typeof value !== 'object') return undefined
if (seen.has(value)) return undefined
if (Array.isArray(value)) {
seen.add(value)
for (const item of value) {
const message = findFirstErrorMessage(item, seen)
if (message) return message
}
return undefined
}
// DOM nodes, class instances, etc. — never treat as error trees
if (!isPlainObject(value)) return undefined
seen.add(value)
if ('message' in value && typeof value.message === 'string' && value.message.trim()) {
return value.message
}
for (const [key, nestedValue] of Object.entries(value)) {
if (key === 'ref') continue
const message = findFirstErrorMessage(nestedValue, seen)
if (message) return message
}
return undefined
}
/** React Hook Form invalid-submit callback used by user-facing forms. */
export function showFormValidationToast<TFieldValues extends FieldValues = FieldValues>(errors: FieldErrors<TFieldValues>) {
addToast({
title: texts.common.formIncomplete,
description: findFirstErrorMessage(errors) ?? texts.common.formValidationDefault,
color: 'danger',
})
}