/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ import { useState, useEffect, type KeyboardEvent } from 'react'; import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Combobox, type ComboboxOption } from '@/components/ui/combobox'; import { Plus, X, Eye, EyeOff, Loader2, Search, Check, ChevronRight } from 'lucide-react'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { ExpressionEditor } from '@/components/expression/ExpressionEditor'; import { OtpAuthField } from '@/components/forms/OtpAuthField'; import { bytesToHuman, humanToBytes, msToHuman, humanToMs, formatSize, formatDuration, SIZE_UNITS, DURATION_UNITS, } from '@/lib/durationFormat'; import { resolveSchema, resolveVariantForm, resolveObject, buildEmbeddedDefaults } from '@/lib/schemaResolver'; import { useAccountStore } from '@/stores/accountStore'; import { useEffectiveEdition } from '@/components/forms/FormEditionContext'; import { useObjectList, useObjectLabel, useNoPermissionMessage, type ObjectOption } from '@/lib/objectOptions'; import { SECRET_MASK } from '@/lib/jmapUtils'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; import { jmapGet, getAccountId } from '@/services/jmap/client'; import type { Field, FormField, Schema, ScalarType, MapValueType, StringFormat, NumberFormat } from '@/types/schema'; export interface FieldWidgetProps { field: Field; formField: FormField; value: unknown; onChange: (value: unknown) => void; readOnly: boolean; error?: string; schema: Schema; } function getRequiredMarker(field: Field, readOnly: boolean): 'required' | 'optional' | null { if (readOnly) return null; if (field.update === 'serverSet') return null; const t = field.type; const eligible = t.type === 'string' || t.type === 'number' || t.type === 'utcDateTime' || t.type === 'enum' || t.type === 'blobId' || t.type === 'objectId'; if (!eligible) return null; if ('nullable' in t && t.nullable) return 'optional'; return 'required'; } export function FieldWidget(props: FieldWidgetProps) { const { t } = useTranslation(); const { field, formField, value, onChange, readOnly, error, schema } = props; const ft = field.type; const edition = useEffectiveEdition(); if (field.enterprise && edition === 'oss') return null; const marker = getRequiredMarker(field, readOnly); const isSelfContained = ft.type === 'object' && ft.objectName === 'x:OtpAuth'; const widget = (() => { switch (ft.type) { case 'string': return ( ); case 'number': return ( ); case 'utcDateTime': return ; case 'boolean': return ; case 'enum': return ( ); case 'blobId': return ; case 'objectId': return ( ); case 'object': return ( ); case 'objectList': return ( ); case 'set': return ( ); case 'map': return ( ); default: { const _: never = ft; void _; return

Unsupported field type

; } } })(); if (isSelfContained) { return (
{widget} {error &&

{error}

}
); } return (
{field.description && (
{field.description.replace(/\\n/g, '\n')}
)} {widget} {error &&

{error}

}
); } interface BufferedInputProps extends Omit, 'onChange' | 'value'> { value: string; onCommit: (v: string) => void; } function BufferedInput({ value, onCommit, onBlur, onKeyDown, ...rest }: BufferedInputProps) { const [local, setLocal] = useState(value); useEffect(() => { setLocal(value); }, [value]); const commit = () => { if (local !== value) onCommit(local); }; return ( setLocal(e.target.value)} onBlur={(e) => { commit(); onBlur?.(e); }} onKeyDown={(e) => { if (e.key === 'Enter' && (rest.type ?? 'text') !== 'textarea') { commit(); } onKeyDown?.(e); }} /> ); } interface BufferedTextareaProps extends Omit, 'onChange' | 'value'> { value: string; onCommit: (v: string) => void; } function BufferedTextarea({ value, onCommit, onBlur, ...rest }: BufferedTextareaProps) { const [local, setLocal] = useState(value); useEffect(() => { setLocal(value); }, [value]); return (