fix admin save state architecture
This commit is contained in:
@@ -14,6 +14,7 @@ import { getLocalizedPath } from "@/lib/locale";
|
|||||||
import { removeManagedMediaFile } from "@/lib/media-storage";
|
import { removeManagedMediaFile } from "@/lib/media-storage";
|
||||||
import { mediaFieldInputSchema } from "@/lib/media-validation";
|
import { mediaFieldInputSchema } from "@/lib/media-validation";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { isCheckedFormValue } from "@/lib/form-data";
|
||||||
import {
|
import {
|
||||||
assetInputSchema,
|
assetInputSchema,
|
||||||
categoryInputSchema,
|
categoryInputSchema,
|
||||||
@@ -40,7 +41,7 @@ function withMessage(pathname: string, type: "success" | "error", message: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCheckboxValue(formData: FormData, key: string) {
|
function normalizeCheckboxValue(formData: FormData, key: string) {
|
||||||
return formData.get(key) === "on";
|
return isCheckedFormValue(formData.get(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonArray(rawValue: FormDataEntryValue | null, key: string) {
|
function parseJsonArray(rawValue: FormDataEntryValue | null, key: string) {
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export default async function RootPortfolioProjectPage({
|
|||||||
logoutAction={logoutAction}
|
logoutAction={logoutAction}
|
||||||
headerTitle={copy.title}
|
headerTitle={copy.title}
|
||||||
headerDescription={copy.subtitle}
|
headerDescription={copy.subtitle}
|
||||||
|
saveFormId="portfolio-project-form"
|
||||||
|
saveButtonLabel={copy.saveProject}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{searchParams?.success ? (
|
{searchParams?.success ? (
|
||||||
@@ -100,7 +102,6 @@ export default async function RootPortfolioProjectPage({
|
|||||||
project={project}
|
project={project}
|
||||||
formId="portfolio-project-form"
|
formId="portfolio-project-form"
|
||||||
redirectPath={`/root/portfolio/projects/${project.id}`}
|
redirectPath={`/root/portfolio/projects/${project.id}`}
|
||||||
submitLabel={copy.saveProject}
|
|
||||||
/>
|
/>
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export default async function RootNewPortfolioProjectPage({
|
|||||||
logoutAction={logoutAction}
|
logoutAction={logoutAction}
|
||||||
headerTitle={copy.title}
|
headerTitle={copy.title}
|
||||||
headerDescription={copy.subtitle}
|
headerDescription={copy.subtitle}
|
||||||
|
saveFormId="portfolio-project-form"
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{searchParams?.error ? (
|
{searchParams?.error ? (
|
||||||
@@ -73,7 +74,6 @@ export default async function RootNewPortfolioProjectPage({
|
|||||||
mediaOptions={mediaOptions}
|
mediaOptions={mediaOptions}
|
||||||
formId="portfolio-project-form"
|
formId="portfolio-project-form"
|
||||||
redirectPath="/root/portfolio/projects/new"
|
redirectPath="/root/portfolio/projects/new"
|
||||||
submitLabel="Projekt anlegen"
|
|
||||||
/>
|
/>
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { redirect } from "next/navigation";
|
|||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect";
|
||||||
|
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
|
import { isCheckedFormValue } from "@/lib/form-data";
|
||||||
import {
|
import {
|
||||||
getMailSettings,
|
getMailSettings,
|
||||||
updateMailSettings,
|
updateMailSettings,
|
||||||
@@ -53,7 +54,7 @@ function parseMailSettingsFormData(
|
|||||||
smtp: {
|
smtp: {
|
||||||
host,
|
host,
|
||||||
port: parsePort(portValue || String(existingSettings.smtp.port)),
|
port: parsePort(portValue || String(existingSettings.smtp.port)),
|
||||||
secure: formData.get("smtpSecure") === "on",
|
secure: isCheckedFormValue(formData.get("smtpSecure")),
|
||||||
username,
|
username,
|
||||||
password: password.trim() ? password : existingSettings.smtp.password,
|
password: password.trim() ? password : existingSettings.smtp.password,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { redirect } from "next/navigation";
|
|||||||
import { isRedirectError } from "next/dist/client/components/redirect";
|
import { isRedirectError } from "next/dist/client/components/redirect";
|
||||||
|
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
|
import { isCheckedFormValue } from "@/lib/form-data";
|
||||||
import {
|
import {
|
||||||
getContactProtectionSettings,
|
getContactProtectionSettings,
|
||||||
updateContactProtectionSettings,
|
updateContactProtectionSettings,
|
||||||
@@ -39,10 +40,10 @@ function parseContactProtectionFormData(
|
|||||||
formData: FormData,
|
formData: FormData,
|
||||||
existingSettings: ContactProtectionSettings,
|
existingSettings: ContactProtectionSettings,
|
||||||
): ContactProtectionSettings {
|
): ContactProtectionSettings {
|
||||||
const turnstileEnabled = formData.get("turnstileEnabled") === "on";
|
const turnstileEnabled = isCheckedFormValue(formData.get("turnstileEnabled"));
|
||||||
const turnstileSiteKey = String(formData.get("turnstileSiteKey") ?? "").trim();
|
const turnstileSiteKey = String(formData.get("turnstileSiteKey") ?? "").trim();
|
||||||
const turnstileSecretKey = String(formData.get("turnstileSecretKey") ?? "");
|
const turnstileSecretKey = String(formData.get("turnstileSecretKey") ?? "");
|
||||||
const rateLimitEnabled = formData.get("contactRateLimitEnabled") === "on";
|
const rateLimitEnabled = isCheckedFormValue(formData.get("contactRateLimitEnabled"));
|
||||||
|
|
||||||
if (turnstileEnabled && !turnstileSiteKey) {
|
if (turnstileEnabled && !turnstileSiteKey) {
|
||||||
throw new Error("Turnstile site key is required when Turnstile is enabled.");
|
throw new Error("Turnstile site key is required when Turnstile is enabled.");
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export function FormSaveButton({
|
|||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const baselineRef = useRef<Map<string, string>>(new Map());
|
const baselineRef = useRef<Map<string, string>>(new Map());
|
||||||
|
const dirtyFormsRef = useRef<Set<string>>(new Set());
|
||||||
const activeFormIdRef = useRef<string | null>(formId ?? null);
|
const activeFormIdRef = useRef<string | null>(formId ?? null);
|
||||||
|
const frameRef = useRef<number | null>(null);
|
||||||
const pendingSubmissionRef = useRef<{
|
const pendingSubmissionRef = useRef<{
|
||||||
formId: string;
|
formId: string;
|
||||||
originUrl: string;
|
originUrl: string;
|
||||||
@@ -54,6 +56,7 @@ export function FormSaveButton({
|
|||||||
setActiveFormId(null);
|
setActiveFormId(null);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
dirtyFormsRef.current.clear();
|
||||||
}
|
}
|
||||||
}, [formId, formIds, formSelector, formSelectors]);
|
}, [formId, formIds, formSelector, formSelectors]);
|
||||||
|
|
||||||
@@ -69,7 +72,12 @@ export function FormSaveButton({
|
|||||||
const forms = Array.from(new Map([...formsById, ...formsBySelector].map((form) => [form.id, form])).values());
|
const forms = Array.from(new Map([...formsById, ...formsBySelector].map((form) => [form.id, form])).values());
|
||||||
|
|
||||||
if (forms.length === 0) {
|
if (forms.length === 0) {
|
||||||
|
if (frameRef.current !== null) {
|
||||||
|
cancelAnimationFrame(frameRef.current);
|
||||||
|
frameRef.current = null;
|
||||||
|
}
|
||||||
baselineRef.current.clear();
|
baselineRef.current.clear();
|
||||||
|
dirtyFormsRef.current.clear();
|
||||||
activeFormIdRef.current = formId ?? null;
|
activeFormIdRef.current = formId ?? null;
|
||||||
setActiveFormId(formId ?? null);
|
setActiveFormId(formId ?? null);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
@@ -80,33 +88,74 @@ export function FormSaveButton({
|
|||||||
const availableIds = forms.map((form) => form.id).filter(Boolean);
|
const availableIds = forms.map((form) => form.id).filter(Boolean);
|
||||||
const fallbackFormId = availableIds[0] ?? null;
|
const fallbackFormId = availableIds[0] ?? null;
|
||||||
|
|
||||||
const readDirtyState = (nextActiveFormId: string | null) => {
|
const selectDirtyFormId = () => {
|
||||||
if (!nextActiveFormId) {
|
const dirtyIds = availableIds.filter((id) => dirtyFormsRef.current.has(id));
|
||||||
setIsDirty(false);
|
|
||||||
|
if (dirtyIds.length === 0) {
|
||||||
|
return activeFormIdRef.current && availableIds.includes(activeFormIdRef.current)
|
||||||
|
? activeFormIdRef.current
|
||||||
|
: fallbackFormId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeFormIdRef.current && dirtyFormsRef.current.has(activeFormIdRef.current)) {
|
||||||
|
return activeFormIdRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirtyIds[0] ?? fallbackFormId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncDirtyState = () => {
|
||||||
|
const nextActiveFormId = selectDirtyFormId();
|
||||||
|
|
||||||
|
activeFormIdRef.current = nextActiveFormId;
|
||||||
|
setActiveFormId(nextActiveFormId);
|
||||||
|
setIsDirty(dirtyFormsRef.current.size > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const evaluateForm = (form: HTMLFormElement) => {
|
||||||
|
const baseline = baselineRef.current.get(form.id);
|
||||||
|
const current = serializeForm(form);
|
||||||
|
|
||||||
|
if (baseline === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextForm = forms.find((form) => form.id === nextActiveFormId);
|
if (current !== baseline) {
|
||||||
|
dirtyFormsRef.current.add(form.id);
|
||||||
if (!nextForm) {
|
|
||||||
setIsDirty(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsDirty(serializeForm(nextForm) !== baselineRef.current.get(nextActiveFormId));
|
dirtyFormsRef.current.delete(form.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleSync = (nextActiveFormId?: string) => {
|
||||||
|
if (nextActiveFormId) {
|
||||||
|
activeFormIdRef.current = nextActiveFormId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameRef.current !== null) {
|
||||||
|
cancelAnimationFrame(frameRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
frameRef.current = requestAnimationFrame(() => {
|
||||||
|
frameRef.current = null;
|
||||||
|
for (const form of forms) {
|
||||||
|
evaluateForm(form);
|
||||||
|
}
|
||||||
|
syncDirtyState();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncBaseline = (form: HTMLFormElement) => {
|
const syncBaseline = (form: HTMLFormElement) => {
|
||||||
baselineRef.current.set(form.id, serializeForm(form));
|
baselineRef.current.set(form.id, serializeForm(form));
|
||||||
readDirtyState(form.id === activeFormIdRef.current ? form.id : activeFormIdRef.current ?? fallbackFormId);
|
dirtyFormsRef.current.delete(form.id);
|
||||||
|
syncDirtyState();
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormActivity = (form: HTMLFormElement) => {
|
const handleFormActivity = (form: HTMLFormElement) => {
|
||||||
activeFormIdRef.current = form.id;
|
|
||||||
setActiveFormId(form.id);
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
setIsDirty(serializeForm(form) !== baselineRef.current.get(form.id));
|
scheduleSync(form.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (form: HTMLFormElement, event: SubmitEvent) => {
|
const handleSubmit = (form: HTMLFormElement, event: SubmitEvent) => {
|
||||||
@@ -162,9 +211,17 @@ export function FormSaveButton({
|
|||||||
: fallbackFormId;
|
: fallbackFormId;
|
||||||
activeFormIdRef.current = nextActive;
|
activeFormIdRef.current = nextActive;
|
||||||
setActiveFormId(nextActive);
|
setActiveFormId(nextActive);
|
||||||
readDirtyState(nextActive);
|
for (const form of forms) {
|
||||||
|
evaluateForm(form);
|
||||||
|
}
|
||||||
|
syncDirtyState();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (frameRef.current !== null) {
|
||||||
|
cancelAnimationFrame(frameRef.current);
|
||||||
|
frameRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
for (const form of forms) {
|
for (const form of forms) {
|
||||||
const handlers = (form as HTMLFormElement & {
|
const handlers = (form as HTMLFormElement & {
|
||||||
__saveButtonHandlers?: {
|
__saveButtonHandlers?: {
|
||||||
@@ -214,6 +271,7 @@ export function FormSaveButton({
|
|||||||
if (hasError) {
|
if (hasError) {
|
||||||
pendingSubmissionRef.current = null;
|
pendingSubmissionRef.current = null;
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
dirtyFormsRef.current.delete(pendingSubmission.formId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ type PortfolioProjectFormProps = {
|
|||||||
project?: PortfolioProjectView | null;
|
project?: PortfolioProjectView | null;
|
||||||
formId: string;
|
formId: string;
|
||||||
redirectPath: string;
|
redirectPath: string;
|
||||||
submitLabel: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const localeFieldConfig = [
|
const localeFieldConfig = [
|
||||||
@@ -286,7 +285,6 @@ export function PortfolioProjectForm({
|
|||||||
project,
|
project,
|
||||||
formId,
|
formId,
|
||||||
redirectPath,
|
redirectPath,
|
||||||
submitLabel,
|
|
||||||
}: PortfolioProjectFormProps) {
|
}: PortfolioProjectFormProps) {
|
||||||
const [activePanel, setActivePanel] = useState<PanelKey>("basic");
|
const [activePanel, setActivePanel] = useState<PanelKey>("basic");
|
||||||
const [projectState, setProjectState] = useState<ProjectFormState>(
|
const [projectState, setProjectState] = useState<ProjectFormState>(
|
||||||
@@ -864,19 +862,6 @@ export function PortfolioProjectForm({
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppCard>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-6 lg:flex-row lg:items-center lg:justify-between">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{validation.allDone
|
|
||||||
? "Alles bereit zum Speichern."
|
|
||||||
: "Du kannst jetzt speichern. Falls Pflichtfelder fehlen, bekommst du oben eine Fehlermeldung."}
|
|
||||||
</p>
|
|
||||||
<Button type="submit">
|
|
||||||
{submitLabel}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</AppCard>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export function SidebarMaintenanceControl({
|
|||||||
return (
|
return (
|
||||||
<form id="sidebar-maintenance-form" action={action} className="space-y-2">
|
<form id="sidebar-maintenance-form" action={action} className="space-y-2">
|
||||||
<input type="hidden" name="redirectPath" value={pathname} />
|
<input type="hidden" name="redirectPath" value={pathname} />
|
||||||
<input type="hidden" name="enabled" value={enabled ? "true" : "false"} />
|
|
||||||
|
|
||||||
<label
|
<label
|
||||||
htmlFor="sidebar-maintenance-enabled"
|
htmlFor="sidebar-maintenance-enabled"
|
||||||
@@ -66,7 +65,10 @@ export function SidebarMaintenanceControl({
|
|||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="sidebar-maintenance-enabled"
|
id="sidebar-maintenance-enabled"
|
||||||
|
name="enabled"
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
|
checkedValue="true"
|
||||||
|
uncheckedValue="false"
|
||||||
onCheckedChange={(checked) => setEnabled(checked === true)}
|
onCheckedChange={(checked) => setEnabled(checked === true)}
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,22 +5,27 @@ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useHiddenInputSync } from "@/components/ui/use-hidden-input-sync";
|
||||||
|
|
||||||
type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
|
type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
checkedValue?: string;
|
||||||
|
uncheckedValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
CheckboxProps
|
CheckboxProps
|
||||||
>(({ className, name, checked, defaultChecked, onCheckedChange, ...props }, ref) => {
|
>(({ className, name, checked, defaultChecked, onCheckedChange, checkedValue = "on", uncheckedValue = "false", ...props }, ref) => {
|
||||||
const [internalChecked, setInternalChecked] = React.useState(defaultChecked === true);
|
const [internalChecked, setInternalChecked] = React.useState(defaultChecked === true);
|
||||||
const isControlled = checked !== undefined;
|
const isControlled = checked !== undefined;
|
||||||
const currentChecked = isControlled ? checked === true : internalChecked;
|
const currentChecked = isControlled ? checked === true : internalChecked;
|
||||||
|
const hiddenValue = currentChecked ? checkedValue : uncheckedValue;
|
||||||
|
const hiddenInputRef = useHiddenInputSync(hiddenValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{name ? <input type="hidden" name={name} value={currentChecked ? "on" : "false"} /> : null}
|
{name ? <input ref={hiddenInputRef} type="hidden" name={name} value={hiddenValue} /> : null}
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useHiddenInputSync } from "@/components/ui/use-hidden-input-sync";
|
||||||
|
|
||||||
type SelectProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & {
|
type SelectProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -23,10 +24,11 @@ function Select({
|
|||||||
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
|
||||||
const isControlled = value !== undefined;
|
const isControlled = value !== undefined;
|
||||||
const currentValue = (isControlled ? value : internalValue) as string;
|
const currentValue = (isControlled ? value : internalValue) as string;
|
||||||
|
const hiddenInputRef = useHiddenInputSync(currentValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{name ? <input type="hidden" name={name} value={currentValue} required={required} /> : null}
|
{name ? <input ref={hiddenInputRef} type="hidden" name={name} value={currentValue} required={required} /> : null}
|
||||||
<SelectPrimitive.Root
|
<SelectPrimitive.Root
|
||||||
value={value}
|
value={value}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function useHiddenInputSync(value: string) {
|
||||||
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||||
|
const mountedRef = React.useRef(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const input = inputRef.current;
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mountedRef.current) {
|
||||||
|
mountedRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return inputRef;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function isCheckedFormValue(value: FormDataEntryValue | null) {
|
||||||
|
return value === "on" || value === "true" || value === "1";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user