Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
61 lines
1.7 KiB
TypeScript
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',
|
|
})
|
|
}
|