fix(reports): remove truncation cap in range collectors to avoid dropped records
feat(calendar): migrate to react-day-picker v9 and polish UI - Update classNames and CSS import (style.css) - Custom Dropdown via shadcn Select - Nav arrows aligned with caption (around) - Today highlight with cyan tone, weekdays in sentence case - Wider layout to avoid overflow; remove inner wrapper chore(tickets): make 'Patrimônio do computador (se houver)' optional - Backend hotfix to enforce optional + label on existing tenants - Hide required asterisk for this field in portal/new-ticket refactor(new-ticket): remove channel dropdown from admin/agent flow - Keep default channel as MANUAL feat(ux): simplify requester section and enlarge combobox trigger - Remove RequesterPreview redundancy; show company badge in trigger
This commit is contained in:
parent
e0ef66555d
commit
a8333c010f
28 changed files with 1752 additions and 455 deletions
|
|
@ -1,7 +1,9 @@
|
|||
"use client"
|
||||
|
||||
import { z } from "zod"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { format, parseISO } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
import type { Doc, Id } from "@/convex/_generated/dataModel"
|
||||
import type { TicketPriority, TicketQueueSummary } from "@/lib/schemas/ticket"
|
||||
import { useMutation, useQuery } from "convex/react"
|
||||
|
|
@ -24,8 +26,13 @@ import { CategorySelectFields } from "@/components/tickets/category-select"
|
|||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { useDefaultQueues } from "@/hooks/use-default-queues"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { normalizeCustomFieldInputs } from "@/lib/ticket-form-helpers"
|
||||
import type { TicketFormDefinition, TicketFormFieldDefinition } from "@/lib/ticket-form-types"
|
||||
import { Calendar as CalendarIcon } from "lucide-react"
|
||||
|
||||
type CustomerOption = {
|
||||
id: string
|
||||
|
|
@ -90,23 +97,6 @@ function RequesterPreview({ customer, company }: RequesterPreviewProps) {
|
|||
|
||||
const NO_COMPANY_VALUE = "__no_company__"
|
||||
|
||||
type TicketFormFieldDefinition = {
|
||||
id: string
|
||||
key: string
|
||||
label: string
|
||||
type: string
|
||||
required: boolean
|
||||
description: string
|
||||
options: Array<{ value: string; label: string }>
|
||||
}
|
||||
|
||||
type TicketFormDefinition = {
|
||||
key: string
|
||||
label: string
|
||||
description: string
|
||||
fields: TicketFormFieldDefinition[]
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
subject: z.string().default(""),
|
||||
summary: z.string().optional(),
|
||||
|
|
@ -175,6 +165,21 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
[companiesRemote]
|
||||
)
|
||||
|
||||
const ensureTicketFormDefaultsMutation = useMutation(api.tickets.ensureTicketFormDefaults)
|
||||
const hasEnsuredFormsRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!convexUserId || hasEnsuredFormsRef.current) return
|
||||
hasEnsuredFormsRef.current = true
|
||||
ensureTicketFormDefaultsMutation({
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
actorId: convexUserId as Id<"users">,
|
||||
}).catch((error) => {
|
||||
console.error("Falha ao preparar formulários personalizados", error)
|
||||
hasEnsuredFormsRef.current = false
|
||||
})
|
||||
}, [convexUserId, ensureTicketFormDefaultsMutation])
|
||||
|
||||
const formsRemote = useQuery(
|
||||
api.tickets.listTicketForms,
|
||||
convexUserId ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip"
|
||||
|
|
@ -195,6 +200,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
|
||||
const [selectedFormKey, setSelectedFormKey] = useState<string>("default")
|
||||
const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({})
|
||||
const [openCalendarField, setOpenCalendarField] = useState<string | null>(null)
|
||||
|
||||
const selectedForm = useMemo(() => forms.find((formDef) => formDef.key === selectedFormKey) ?? forms[0], [forms, selectedFormKey])
|
||||
|
||||
|
|
@ -225,7 +231,6 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
[attachments]
|
||||
)
|
||||
const priorityValue = form.watch("priority") as TicketPriority
|
||||
const channelValue = form.watch("channel")
|
||||
const queueValue = form.watch("queueName") ?? "NONE"
|
||||
const assigneeValue = form.watch("assigneeId") ?? null
|
||||
const assigneeSelectValue = assigneeValue ?? "NONE"
|
||||
|
|
@ -386,6 +391,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
useEffect(() => {
|
||||
if (!open) {
|
||||
setAssigneeInitialized(false)
|
||||
setOpenCalendarField(null)
|
||||
return
|
||||
}
|
||||
if (assigneeInitialized) return
|
||||
|
|
@ -449,49 +455,13 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
|
||||
let customFieldsPayload: Array<{ fieldId: Id<"ticketFields">; value: unknown }> = []
|
||||
if (selectedFormKey !== "default" && selectedForm?.fields?.length) {
|
||||
for (const field of selectedForm.fields) {
|
||||
const raw = customFieldValues[field.id]
|
||||
const isBooleanField = field.type === "boolean"
|
||||
const isEmpty =
|
||||
raw === undefined ||
|
||||
raw === null ||
|
||||
(typeof raw === "string" && raw.trim().length === 0)
|
||||
|
||||
if (isBooleanField) {
|
||||
const boolValue = Boolean(raw)
|
||||
customFieldsPayload.push({ fieldId: field.id as Id<"ticketFields">, value: boolValue })
|
||||
continue
|
||||
}
|
||||
|
||||
if (field.required && isEmpty) {
|
||||
toast.error(`Preencha o campo "${field.label}".`, { id: "new-ticket" })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
continue
|
||||
}
|
||||
|
||||
let value: unknown = raw
|
||||
if (field.type === "number") {
|
||||
const parsed = typeof raw === "number" ? raw : Number(raw)
|
||||
if (!Number.isFinite(parsed)) {
|
||||
toast.error(`Informe um valor numérico válido para "${field.label}".`, { id: "new-ticket" })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
value = parsed
|
||||
} else if (field.type === "boolean") {
|
||||
value = Boolean(raw)
|
||||
} else if (field.type === "date") {
|
||||
value = String(raw)
|
||||
} else {
|
||||
value = String(raw)
|
||||
}
|
||||
|
||||
customFieldsPayload.push({ fieldId: field.id as Id<"ticketFields">, value })
|
||||
const normalized = normalizeCustomFieldInputs(selectedForm.fields, customFieldValues)
|
||||
if (!normalized.ok) {
|
||||
toast.error(normalized.message, { id: "new-ticket" })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
customFieldsPayload = normalized.payload
|
||||
}
|
||||
setLoading(true)
|
||||
toast.loading("Criando ticket…", { id: "new-ticket" })
|
||||
|
|
@ -601,7 +571,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
</div>
|
||||
{forms.length > 1 ? (
|
||||
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-4">
|
||||
<p className="text-sm font-semibold text-neutral-800">Modelo de ticket</p>
|
||||
<p className="text-sm font-semibold text-neutral-800">Tipo de solicitação</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{forms.map((formDef) => (
|
||||
<Button
|
||||
|
|
@ -744,7 +714,6 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
<FieldLabel className="flex items-center gap-1">
|
||||
Solicitante <span className="text-destructive">*</span>
|
||||
</FieldLabel>
|
||||
<RequesterPreview customer={selectedRequester} company={selectedCompanyOption} />
|
||||
<SearchableCombobox
|
||||
value={requesterValue || null}
|
||||
onValueChange={(nextValue) => {
|
||||
|
|
@ -778,18 +747,27 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
placeholder={filteredCustomers.length === 0 ? "Nenhum usuário disponível" : "Selecionar solicitante"}
|
||||
searchPlaceholder="Buscar por nome ou e-mail..."
|
||||
disabled={filteredCustomers.length === 0}
|
||||
renderValue={(option) =>
|
||||
option ? (
|
||||
<div className="flex flex-col">
|
||||
<span className="truncate font-medium text-foreground">{option.label}</span>
|
||||
{option.description ? (
|
||||
<span className="truncate text-xs text-muted-foreground">{option.description}</span>
|
||||
renderValue={(option) => {
|
||||
if (!option) return <span className="text-muted-foreground">Selecionar solicitante</span>
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate font-medium text-foreground">{option.label}</span>
|
||||
{option.description ? (
|
||||
<span className="truncate text-xs text-muted-foreground">{option.description}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{selectedCompanyOption && selectedCompanyOption.id !== NO_COMPANY_VALUE ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="hidden shrink-0 rounded-full px-2.5 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground sm:inline-flex"
|
||||
>
|
||||
{selectedCompanyOption.name}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Selecionar solicitante</span>
|
||||
)
|
||||
}
|
||||
}}
|
||||
renderOption={(option) => {
|
||||
const record = requesterById.get(option.value)
|
||||
const initials = getInitials(record?.name, record?.email ?? option.label)
|
||||
|
|
@ -862,34 +840,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Canal</FieldLabel>
|
||||
<Select value={channelValue} onValueChange={(v) => form.setValue("channel", v as z.infer<typeof schema>["channel"])}>
|
||||
<SelectTrigger className={selectTriggerClass}>
|
||||
<SelectValue placeholder="Canal" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border border-slate-200 bg-white text-neutral-800 shadow-md">
|
||||
<SelectItem value="EMAIL" className={selectItemClass}>
|
||||
E-mail
|
||||
</SelectItem>
|
||||
<SelectItem value="WHATSAPP" className={selectItemClass}>
|
||||
WhatsApp
|
||||
</SelectItem>
|
||||
<SelectItem value="CHAT" className={selectItemClass}>
|
||||
Chat
|
||||
</SelectItem>
|
||||
<SelectItem value="PHONE" className={selectItemClass}>
|
||||
Telefone
|
||||
</SelectItem>
|
||||
<SelectItem value="API" className={selectItemClass}>
|
||||
API
|
||||
</SelectItem>
|
||||
<SelectItem value="MANUAL" className={selectItemClass}>
|
||||
Manual
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{/* Canal removido da UI: padrão MANUAL será enviado */}
|
||||
<Field>
|
||||
<FieldLabel>Fila</FieldLabel>
|
||||
<Select value={queueValue} onValueChange={(v) => form.setValue("queueName", v === "NONE" ? null : v)}>
|
||||
|
|
@ -935,119 +886,180 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
|
|||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{selectedFormKey !== "default" && selectedForm.fields.length > 0 ? (
|
||||
<div className="space-y-4 rounded-xl border border-slate-200 bg-white px-4 py-4">
|
||||
<p className="text-sm font-semibold text-neutral-800">Informações adicionais</p>
|
||||
{selectedForm.fields.map((field) => {
|
||||
const value = customFieldValues[field.id]
|
||||
const fieldId = `custom-field-${field.id}`
|
||||
const labelSuffix = field.required ? <span className="text-destructive">*</span> : null
|
||||
const helpText = field.description ? (
|
||||
<p className="text-xs text-neutral-500">{field.description}</p>
|
||||
) : null
|
||||
|
||||
if (field.type === "boolean") {
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className="flex items-center gap-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2"
|
||||
>
|
||||
<input
|
||||
id={fieldId}
|
||||
type="checkbox"
|
||||
className="size-4 rounded border border-slate-300 text-[#00d6eb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00d6eb]/40"
|
||||
checked={Boolean(value)}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.checked)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor={fieldId} className="text-sm font-medium text-neutral-800">
|
||||
{field.label} {labelSuffix}
|
||||
</label>
|
||||
{helpText}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === "select") {
|
||||
return (
|
||||
<Field key={field.id}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onValueChange={(selected) => handleCustomFieldChange(field, selected)}
|
||||
>
|
||||
<SelectTrigger className={selectTriggerClass}>
|
||||
<SelectValue placeholder="Selecione" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border border-slate-200 bg-white text-neutral-800 shadow-md">
|
||||
{field.options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value} className={selectItemClass}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === "number") {
|
||||
return (
|
||||
<Field key={field.id}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={fieldId}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
value={typeof value === "number" || typeof value === "string" ? String(value) : ""}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.value)}
|
||||
/>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === "date") {
|
||||
return (
|
||||
<Field key={field.id}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={fieldId}
|
||||
type="date"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.value)}
|
||||
/>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Field key={field.id}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={fieldId}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.value)}
|
||||
/>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{selectedFormKey !== "default" && selectedForm.fields.length > 0 ? (
|
||||
<div className="grid gap-4 rounded-xl border border-slate-200 bg-white px-4 py-4 sm:grid-cols-2 lg:col-span-2">
|
||||
<p className="text-sm font-semibold text-neutral-800 sm:col-span-2">Informações adicionais</p>
|
||||
{selectedForm.fields.map((field) => {
|
||||
const value = customFieldValues[field.id]
|
||||
const fieldId = `custom-field-${field.id}`
|
||||
const isRequiredStar = field.required && field.key !== "colaborador_patrimonio"
|
||||
const labelSuffix = isRequiredStar ? <span className="text-destructive">*</span> : null
|
||||
const helpText = field.description ? (
|
||||
<p className="text-xs text-neutral-500">{field.description}</p>
|
||||
) : null
|
||||
const shouldUseTextarea = field.key.includes("observacao") || field.key.includes("permissao")
|
||||
const spanClass = shouldUseTextarea || field.type === "boolean" ? "sm:col-span-2" : ""
|
||||
|
||||
if (field.type === "boolean") {
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2",
|
||||
spanClass,
|
||||
"sm:col-span-2"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
id={fieldId}
|
||||
type="checkbox"
|
||||
className="size-4 rounded border border-slate-300 text-[#00d6eb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00d6eb]/40"
|
||||
checked={Boolean(value)}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.checked)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor={fieldId} className="text-sm font-medium text-neutral-800">
|
||||
{field.label} {labelSuffix}
|
||||
</label>
|
||||
{helpText}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === "select") {
|
||||
return (
|
||||
<Field key={field.id} className={spanClass}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onValueChange={(selected) => handleCustomFieldChange(field, selected)}
|
||||
>
|
||||
<SelectTrigger className={selectTriggerClass}>
|
||||
<SelectValue placeholder="Selecione" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl border border-slate-200 bg-white text-neutral-800 shadow-md">
|
||||
{field.options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value} className={selectItemClass}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === "number") {
|
||||
return (
|
||||
<Field key={field.id} className={spanClass}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={fieldId}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
value={typeof value === "number" || typeof value === "string" ? String(value) : ""}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.value)}
|
||||
/>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === "date") {
|
||||
const parsedDate =
|
||||
typeof value === "string" && value ? parseISO(value) : undefined
|
||||
const isValidDate = Boolean(parsedDate && !Number.isNaN(parsedDate.getTime()))
|
||||
return (
|
||||
<Field key={field.id} className={spanClass}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Popover
|
||||
open={openCalendarField === field.id}
|
||||
onOpenChange={(open) => setOpenCalendarField(open ? field.id : null)}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between gap-2 text-left font-normal",
|
||||
!isValidDate && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{isValidDate
|
||||
? format(parsedDate as Date, "dd/MM/yyyy", { locale: ptBR })
|
||||
: "Selecionar data"}
|
||||
</span>
|
||||
<CalendarIcon className="size-4 text-muted-foreground" aria-hidden="true" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={isValidDate ? (parsedDate as Date) : undefined}
|
||||
onSelect={(selected) => {
|
||||
handleCustomFieldChange(
|
||||
field,
|
||||
selected ? format(selected, "yyyy-MM-dd") : ""
|
||||
)
|
||||
setOpenCalendarField(null)
|
||||
}}
|
||||
initialFocus
|
||||
captionLayout="dropdown"
|
||||
startMonth={new Date(1900, 0)}
|
||||
endMonth={new Date(new Date().getFullYear() + 5, 11)}
|
||||
locale={ptBR}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldUseTextarea) {
|
||||
return (
|
||||
<Field key={field.id} className={cn("flex-col", spanClass, "sm:col-span-2")}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<textarea
|
||||
id={fieldId}
|
||||
className="min-h-[90px] rounded-lg border border-slate-300 px-3 py-2 text-sm text-neutral-800 shadow-sm focus:border-neutral-900 focus:outline-none focus:ring-2 focus:ring-neutral-900/10"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.value)}
|
||||
/>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Field key={field.id} className={spanClass}>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
{field.label} {labelSuffix}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={fieldId}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(event) => handleCustomFieldChange(field, event.target.value)}
|
||||
/>
|
||||
{helpText}
|
||||
</Field>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</form>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue