mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
528 lines
24 KiB
TypeScript
528 lines
24 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
ActionIcon,
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
Checkbox,
|
|
Divider,
|
|
Drawer,
|
|
Group,
|
|
Modal,
|
|
Paper,
|
|
Select,
|
|
SimpleGrid,
|
|
Stack,
|
|
Switch,
|
|
Text,
|
|
Textarea,
|
|
TextInput,
|
|
ThemeIcon,
|
|
Title,
|
|
} from '@mantine/core';
|
|
import {
|
|
IconArrowDown,
|
|
IconArrowUp,
|
|
IconEdit,
|
|
IconFilePlus,
|
|
IconGripVertical,
|
|
IconPlus,
|
|
IconSettings,
|
|
IconTrash,
|
|
} from '@tabler/icons-react';
|
|
import { notify, ModalFooter } from '@ema-platform/ui';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
type FieldType = 'text' | 'number' | 'select' | 'textarea' | 'date' | 'file';
|
|
type FieldStep = 'Vessel Details' | 'Technical & Ownership' | 'Documents';
|
|
|
|
interface FormField {
|
|
id: string;
|
|
label: string;
|
|
key: string;
|
|
type: FieldType;
|
|
step: FieldStep;
|
|
required: boolean;
|
|
placeholder: string;
|
|
helpText: string;
|
|
options: string; // comma-separated for select type
|
|
enabled: boolean;
|
|
system: boolean; // system fields cannot be deleted
|
|
}
|
|
|
|
const FIELD_TYPES: { value: FieldType; label: string }[] = [
|
|
{ value: 'text', label: 'Text Input' },
|
|
{ value: 'number', label: 'Number Input' },
|
|
{ value: 'select', label: 'Dropdown / Select' },
|
|
{ value: 'textarea', label: 'Text Area' },
|
|
{ value: 'date', label: 'Date Picker' },
|
|
{ value: 'file', label: 'File Upload' },
|
|
];
|
|
|
|
const STEPS: FieldStep[] = ['Vessel Details', 'Technical & Ownership', 'Documents'];
|
|
|
|
const TYPE_COLOR: Record<FieldType, string> = {
|
|
text: 'blue',
|
|
number: 'orange',
|
|
select: 'violet',
|
|
textarea: 'gray',
|
|
date: 'teal',
|
|
file: 'green',
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Default form schema — mirrors the vessel registration wizard
|
|
// ---------------------------------------------------------------------------
|
|
const DEFAULT_FIELDS: FormField[] = [
|
|
// Vessel Details
|
|
{ id: 'f1', label: 'Vessel Name', key: 'vesselName', type: 'text', step: 'Vessel Details', required: true, placeholder: 'e.g. Lake Tana Star', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f2', label: 'Vessel Type', key: 'vesselType', type: 'select', step: 'Vessel Details', required: true, placeholder: 'Select type', helpText: 'Options depend on selected category', options: 'Passenger Ferry,Cargo Barge,Fishing Vessel,Tug Boat,Container Ship,Bulk Carrier,Tanker', enabled: true, system: true },
|
|
{ id: 'f3', label: 'Registration Area', key: 'registrationArea', type: 'text', step: 'Vessel Details', required: true, placeholder: 'e.g. Bahir Dar', helpText: 'Port or lake authority area', options: '', enabled: true, system: true },
|
|
{ id: 'f4', label: 'Flag State', key: 'flagState', type: 'text', step: 'Vessel Details', required: true, placeholder: 'e.g. Ethiopia', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f5', label: 'Passenger Capacity / Gross Tonnage',key: 'capacityValue', type: 'number', step: 'Vessel Details', required: true, placeholder: 'Enter value', helpText: 'Passenger count for ferries; GT for cargo', options: '', enabled: true, system: true },
|
|
{ id: 'f6', label: 'Vessel Length (m)', key: 'vesselLengthM', type: 'number', step: 'Vessel Details', required: true, placeholder: 'e.g. 32', helpText: '', options: '', enabled: true, system: false },
|
|
// Technical & Ownership
|
|
{ id: 'f7', label: 'IMO / Hull Number', key: 'imoOrHullNumber', type: 'text', step: 'Technical & Ownership',required: true, placeholder: 'e.g. IMO9876543', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f8', label: 'Manufacturer / Shipyard', key: 'manufacturerShipyard',type: 'text', step: 'Technical & Ownership',required: true, placeholder: 'e.g. Hyundai Heavy Industries', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f9', label: 'Year Built', key: 'yearBuilt', type: 'number', step: 'Technical & Ownership',required: true, placeholder: 'e.g. 2015', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f10', label: 'Engine Type', key: 'engineType', type: 'select', step: 'Technical & Ownership',required: true, placeholder: 'Select engine type', helpText: '', options: 'Diesel Engine,Dual-Fuel Engine,Electric Motor,Hybrid Diesel-Electric,Steam Turbine,Gas Turbine,Outboard Motor', enabled: true, system: true },
|
|
{ id: 'f11', label: 'Engine Power (kW)', key: 'enginePowerKw', type: 'number', step: 'Technical & Ownership',required: true, placeholder: 'e.g. 450', helpText: '', options: '', enabled: true, system: false },
|
|
{ id: 'f12', label: 'Number of Engines', key: 'numberOfEngines', type: 'number', step: 'Technical & Ownership',required: true, placeholder: 'e.g. 2', helpText: '', options: '', enabled: true, system: false },
|
|
{ id: 'f13', label: 'Hull Material', key: 'hullMaterial', type: 'select', step: 'Technical & Ownership',required: true, placeholder: 'Select material', helpText: '', options: 'Steel,Aluminum,Fiberglass/GRP,Wood,Ferro-Cement', enabled: true, system: false },
|
|
{ id: 'f14', label: 'Owner Full Name', key: 'ownerName', type: 'text', step: 'Technical & Ownership',required: true, placeholder: 'Legal name of the owner', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f15', label: 'National ID / TIN', key: 'ownerNationalIdOrTin',type: 'text', step: 'Technical & Ownership',required: true, placeholder: 'e.g. ET-0000000', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f16', label: 'Owner Phone', key: 'ownerPhone', type: 'text', step: 'Technical & Ownership',required: true, placeholder: '+251 9XX XXX XXX', helpText: '', options: '', enabled: true, system: true },
|
|
{ id: 'f17', label: 'Owner Address', key: 'ownerAddress', type: 'textarea', step: 'Technical & Ownership',required: false, placeholder: 'City, Region', helpText: '', options: '', enabled: true, system: false },
|
|
// Documents
|
|
{ id: 'f18', label: 'Vessel Photos (min. 2)', key: 'vesselPhotos', type: 'file', step: 'Documents', required: true, placeholder: '', helpText: 'Required for all vessel types', options: '', enabled: true, system: true },
|
|
{ id: 'f19', label: 'Proof of Ownership / Bill of Sale', key: 'proofOfOwnership', type: 'file', step: 'Documents', required: true, placeholder: '', helpText: 'Sea-going vessels only', options: '', enabled: true, system: false },
|
|
{ id: 'f20', label: 'Ship Particulars', key: 'shipParticulars', type: 'file', step: 'Documents', required: true, placeholder: '', helpText: 'Sea-going vessels only', options: '', enabled: true, system: false },
|
|
{ id: 'f21', label: 'Insurance Certificate', key: 'insuranceCertificate',type: 'file', step: 'Documents', required: true, placeholder: '', helpText: 'Sea-going vessels only', options: '', enabled: true, system: false },
|
|
];
|
|
|
|
function generateId() {
|
|
return 'f' + Date.now();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Field row card
|
|
// ---------------------------------------------------------------------------
|
|
function FieldCard({
|
|
field,
|
|
index,
|
|
total,
|
|
onEdit,
|
|
onDelete,
|
|
onToggle,
|
|
onMoveUp,
|
|
onMoveDown,
|
|
}: {
|
|
field: FormField;
|
|
index: number;
|
|
total: number;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onToggle: () => void;
|
|
onMoveUp: () => void;
|
|
onMoveDown: () => void;
|
|
}) {
|
|
return (
|
|
<Card
|
|
withBorder
|
|
radius="sm"
|
|
p="sm"
|
|
style={{
|
|
opacity: field.enabled ? 1 : 0.5,
|
|
borderColor: field.enabled ? 'var(--mantine-color-default-border)' : 'var(--mantine-color-gray-3)',
|
|
}}
|
|
>
|
|
<Group justify="space-between" wrap="nowrap">
|
|
<Group gap="sm" wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
|
|
<ActionIcon variant="subtle" color="gray" size="sm" style={{ cursor: 'grab', flexShrink: 0 }}>
|
|
<IconGripVertical size={14} />
|
|
</ActionIcon>
|
|
<div style={{ minWidth: 0 }}>
|
|
<Group gap="xs" wrap="nowrap">
|
|
<Text fz="sm" fw={600} truncate>{field.label}</Text>
|
|
{field.required && <Text fz="xs" c="red" fw={700}>*</Text>}
|
|
{field.system && <Badge size="xs" color="gray" variant="outline">system</Badge>}
|
|
</Group>
|
|
<Group gap="xs" mt={2}>
|
|
<Badge size="xs" color={TYPE_COLOR[field.type]} variant="light">{field.type}</Badge>
|
|
<Text fz="xs" c="dimmed" truncate>key: {field.key}</Text>
|
|
{field.helpText && <Text fz="xs" c="dimmed" truncate>· {field.helpText}</Text>}
|
|
</Group>
|
|
</div>
|
|
</Group>
|
|
<Group gap={4} wrap="nowrap" style={{ flexShrink: 0 }}>
|
|
<Switch
|
|
size="xs"
|
|
checked={field.enabled}
|
|
onChange={onToggle}
|
|
title={field.enabled ? 'Disable field' : 'Enable field'}
|
|
/>
|
|
<ActionIcon variant="subtle" color="gray" size="sm" disabled={index === 0} onClick={onMoveUp}>
|
|
<IconArrowUp size={14} />
|
|
</ActionIcon>
|
|
<ActionIcon variant="subtle" color="gray" size="sm" disabled={index === total - 1} onClick={onMoveDown}>
|
|
<IconArrowDown size={14} />
|
|
</ActionIcon>
|
|
<ActionIcon variant="subtle" color="blue" size="sm" onClick={onEdit}>
|
|
<IconEdit size={14} />
|
|
</ActionIcon>
|
|
<ActionIcon
|
|
variant="subtle"
|
|
color="red"
|
|
size="sm"
|
|
disabled={field.system}
|
|
onClick={onDelete}
|
|
title={field.system ? 'System fields cannot be deleted' : 'Delete field'}
|
|
>
|
|
<IconTrash size={14} />
|
|
</ActionIcon>
|
|
</Group>
|
|
</Group>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main page
|
|
// ---------------------------------------------------------------------------
|
|
export function VesselRegistrationFormBuilderPage() {
|
|
const [fields, setFields] = useState<FormField[]>(DEFAULT_FIELDS);
|
|
const [activeStep, setActiveStep] = useState<FieldStep>('Vessel Details');
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
const [editing, setEditing] = useState<FormField | null>(null);
|
|
const [isNew, setIsNew] = useState(false);
|
|
const [deleteTarget, setDeleteTarget] = useState<FormField | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// Draft state for the editor drawer
|
|
const [draftLabel, setDraftLabel] = useState('');
|
|
const [draftKey, setDraftKey] = useState('');
|
|
const [draftType, setDraftType] = useState<FieldType>('text');
|
|
const [draftStep, setDraftStep] = useState<FieldStep>('Vessel Details');
|
|
const [draftRequired, setDraftRequired] = useState(false);
|
|
const [draftPlaceholder, setDraftPlaceholder] = useState('');
|
|
const [draftHelpText, setDraftHelpText] = useState('');
|
|
const [draftOptions, setDraftOptions] = useState('');
|
|
|
|
const stepFields = fields.filter((f) => f.step === activeStep);
|
|
const totalEnabled = fields.filter((f) => f.enabled).length;
|
|
|
|
const openEdit = (field: FormField) => {
|
|
setEditing(field);
|
|
setIsNew(false);
|
|
setDraftLabel(field.label);
|
|
setDraftKey(field.key);
|
|
setDraftType(field.type);
|
|
setDraftStep(field.step);
|
|
setDraftRequired(field.required);
|
|
setDraftPlaceholder(field.placeholder);
|
|
setDraftHelpText(field.helpText);
|
|
setDraftOptions(field.options);
|
|
setDrawerOpen(true);
|
|
};
|
|
|
|
const openNew = () => {
|
|
setEditing(null);
|
|
setIsNew(true);
|
|
setDraftLabel('');
|
|
setDraftKey('');
|
|
setDraftType('text');
|
|
setDraftStep(activeStep);
|
|
setDraftRequired(false);
|
|
setDraftPlaceholder('');
|
|
setDraftHelpText('');
|
|
setDraftOptions('');
|
|
setDrawerOpen(true);
|
|
};
|
|
|
|
const saveField = () => {
|
|
if (!draftLabel.trim() || !draftKey.trim()) {
|
|
notify.error('Label and field key are required.');
|
|
return;
|
|
}
|
|
if (isNew) {
|
|
const newField: FormField = {
|
|
id: generateId(),
|
|
label: draftLabel.trim(),
|
|
key: draftKey.trim().replace(/\s+/g, '_').toLowerCase(),
|
|
type: draftType,
|
|
step: draftStep,
|
|
required: draftRequired,
|
|
placeholder: draftPlaceholder,
|
|
helpText: draftHelpText,
|
|
options: draftOptions,
|
|
enabled: true,
|
|
system: false,
|
|
};
|
|
setFields((prev) => [...prev, newField]);
|
|
notify.success('Field added.');
|
|
} else if (editing) {
|
|
setFields((prev) =>
|
|
prev.map((f) =>
|
|
f.id === editing.id
|
|
? {
|
|
...f,
|
|
label: draftLabel.trim(),
|
|
key: draftKey.trim().replace(/\s+/g, '_').toLowerCase(),
|
|
type: draftType,
|
|
step: draftStep,
|
|
required: draftRequired,
|
|
placeholder: draftPlaceholder,
|
|
helpText: draftHelpText,
|
|
options: draftOptions,
|
|
}
|
|
: f
|
|
)
|
|
);
|
|
notify.success('Field updated.');
|
|
}
|
|
setDrawerOpen(false);
|
|
};
|
|
|
|
const toggleField = (id: string) => {
|
|
setFields((prev) => prev.map((f) => (f.id === id ? { ...f, enabled: !f.enabled } : f)));
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (!deleteTarget) return;
|
|
setFields((prev) => prev.filter((f) => f.id !== deleteTarget.id));
|
|
setDeleteTarget(null);
|
|
notify.success('Field removed.');
|
|
};
|
|
|
|
const moveField = (id: string, direction: 'up' | 'down') => {
|
|
setFields((prev) => {
|
|
const stepList = prev.filter((f) => f.step === activeStep);
|
|
const otherList = prev.filter((f) => f.step !== activeStep);
|
|
const idx = stepList.findIndex((f) => f.id === id);
|
|
if (idx < 0) return prev;
|
|
const next = [...stepList];
|
|
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
|
|
if (swapIdx < 0 || swapIdx >= next.length) return prev;
|
|
[next[idx], next[swapIdx]] = [next[swapIdx], next[idx]];
|
|
// Rebuild with original order for other steps
|
|
return prev.map((f) => {
|
|
if (f.step !== activeStep) return f;
|
|
return next.shift()!;
|
|
});
|
|
});
|
|
};
|
|
|
|
const handleSaveSchema = async () => {
|
|
setSaving(true);
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
setSaving(false);
|
|
notify.success('Form schema saved successfully.');
|
|
};
|
|
|
|
const stepCounts = STEPS.map((s) => ({
|
|
step: s,
|
|
total: fields.filter((f) => f.step === s).length,
|
|
enabled: fields.filter((f) => f.step === s && f.enabled).length,
|
|
}));
|
|
|
|
return (
|
|
<Stack gap="md">
|
|
{/* Header */}
|
|
<Group justify="space-between">
|
|
<Group gap="sm">
|
|
<ThemeIcon size={44} radius="md" color="teal" variant="light">
|
|
<IconSettings size={24} />
|
|
</ThemeIcon>
|
|
<div>
|
|
<Title order={2}>Vessel Registration Form Builder</Title>
|
|
<Text fz="sm" c="dimmed">Add, edit, reorder, or disable fields on the vessel registration form</Text>
|
|
</div>
|
|
</Group>
|
|
<Group gap="sm">
|
|
<Button leftSection={<IconPlus size={15} />} variant="default" onClick={openNew}>
|
|
Add Field
|
|
</Button>
|
|
<Button leftSection={<IconFilePlus size={15} />} color="teal" loading={saving} onClick={handleSaveSchema}>
|
|
Save Schema
|
|
</Button>
|
|
</Group>
|
|
</Group>
|
|
|
|
{/* Summary stats */}
|
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="sm">
|
|
{[
|
|
{ label: 'Total Fields', value: fields.length, color: 'teal' },
|
|
{ label: 'Active Fields', value: totalEnabled, color: 'blue' },
|
|
{ label: 'Disabled Fields', value: fields.length - totalEnabled, color: 'gray' },
|
|
{ label: 'Required Fields', value: fields.filter((f) => f.required && f.enabled).length, color: 'orange' },
|
|
].map((s) => (
|
|
<Card key={s.label} withBorder radius="md" p="md">
|
|
<Text fz="xl" fw={800} c={`${s.color}.6`}>{s.value}</Text>
|
|
<Text fz="xs" c="dimmed">{s.label}</Text>
|
|
</Card>
|
|
))}
|
|
</SimpleGrid>
|
|
|
|
{/* Step tabs */}
|
|
<Group gap="sm">
|
|
{stepCounts.map(({ step, total, enabled }) => (
|
|
<Button
|
|
key={step}
|
|
variant={activeStep === step ? 'filled' : 'default'}
|
|
color={activeStep === step ? 'teal' : undefined}
|
|
size="sm"
|
|
onClick={() => setActiveStep(step)}
|
|
rightSection={
|
|
<Badge
|
|
size="xs"
|
|
color={activeStep === step ? 'white' : 'gray'}
|
|
variant="filled"
|
|
circle
|
|
>
|
|
{enabled}/{total}
|
|
</Badge>
|
|
}
|
|
>
|
|
{step}
|
|
</Button>
|
|
))}
|
|
</Group>
|
|
|
|
{/* Field list */}
|
|
<Paper withBorder radius="md" p="md">
|
|
<Group justify="space-between" mb="md">
|
|
<Text fw={700} fz="sm">{activeStep}</Text>
|
|
<Text fz="xs" c="dimmed">
|
|
{stepFields.filter((f) => f.enabled).length} of {stepFields.length} fields active
|
|
</Text>
|
|
</Group>
|
|
|
|
{stepFields.length === 0 ? (
|
|
<Text fz="sm" c="dimmed" ta="center" py="xl">
|
|
No fields in this step. Click "Add Field" to add one.
|
|
</Text>
|
|
) : (
|
|
<Stack gap="xs">
|
|
{stepFields.map((field, i) => (
|
|
<FieldCard
|
|
key={field.id}
|
|
field={field}
|
|
index={i}
|
|
total={stepFields.length}
|
|
onEdit={() => openEdit(field)}
|
|
onDelete={() => setDeleteTarget(field)}
|
|
onToggle={() => toggleField(field.id)}
|
|
onMoveUp={() => moveField(field.id, 'up')}
|
|
onMoveDown={() => moveField(field.id, 'down')}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
)}
|
|
</Paper>
|
|
|
|
{/* Edit / Add Drawer */}
|
|
<Drawer
|
|
opened={drawerOpen}
|
|
onClose={() => setDrawerOpen(false)}
|
|
position="right"
|
|
size="md"
|
|
title={<Text fw={700}>{isNew ? 'Add New Field' : `Edit — ${editing?.label}`}</Text>}
|
|
>
|
|
<Stack gap="md">
|
|
<TextInput
|
|
label="Field Label"
|
|
placeholder="e.g. Vessel Name"
|
|
required
|
|
value={draftLabel}
|
|
onChange={(e) => {
|
|
const val = e.currentTarget.value;
|
|
setDraftLabel(val);
|
|
if (isNew)
|
|
setDraftKey(val.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, ''));
|
|
}}
|
|
/>
|
|
<TextInput
|
|
label="Field Key (internal)"
|
|
placeholder="e.g. vessel_name"
|
|
required
|
|
value={draftKey}
|
|
onChange={(e) =>
|
|
setDraftKey(
|
|
e.currentTarget.value.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '')
|
|
)
|
|
}
|
|
description="Used as the API field name. Letters, numbers and underscores only."
|
|
/>
|
|
<Select
|
|
label="Field Type"
|
|
data={FIELD_TYPES}
|
|
value={draftType}
|
|
onChange={(v) => setDraftType((v as FieldType) ?? 'text')}
|
|
/>
|
|
<Select
|
|
label="Step / Section"
|
|
data={STEPS.map((s) => ({ value: s, label: s }))}
|
|
value={draftStep}
|
|
onChange={(v) => setDraftStep((v as FieldStep) ?? 'Vessel Details')}
|
|
/>
|
|
<TextInput
|
|
label="Placeholder Text"
|
|
placeholder="e.g. Enter vessel name"
|
|
value={draftPlaceholder}
|
|
onChange={(e) => setDraftPlaceholder(e.currentTarget.value)}
|
|
/>
|
|
<TextInput
|
|
label="Help Text"
|
|
placeholder="Short hint shown below the field"
|
|
value={draftHelpText}
|
|
onChange={(e) => setDraftHelpText(e.currentTarget.value)}
|
|
/>
|
|
{draftType === 'select' && (
|
|
<Textarea
|
|
label="Options (comma-separated)"
|
|
placeholder="Option A,Option B,Option C"
|
|
value={draftOptions}
|
|
onChange={(e) => setDraftOptions(e.currentTarget.value)}
|
|
rows={3}
|
|
description="Each comma-separated value becomes a dropdown option."
|
|
/>
|
|
)}
|
|
<Checkbox
|
|
label="Required field"
|
|
checked={draftRequired}
|
|
onChange={(e) => setDraftRequired(e.currentTarget.checked)}
|
|
/>
|
|
<Divider />
|
|
<ModalFooter>
|
|
<Button variant="default" onClick={() => setDrawerOpen(false)}>Cancel</Button>
|
|
<Button color="teal" onClick={saveField}>
|
|
{isNew ? 'Add Field' : 'Save Changes'}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Stack>
|
|
</Drawer>
|
|
|
|
{/* Delete confirm modal */}
|
|
<Modal
|
|
opened={!!deleteTarget}
|
|
onClose={() => setDeleteTarget(null)}
|
|
title="Delete Field"
|
|
size="sm"
|
|
>
|
|
<Stack gap="md">
|
|
<Text fz="sm">
|
|
Are you sure you want to remove <strong>{deleteTarget?.label}</strong> from the form?
|
|
This cannot be undone.
|
|
</Text>
|
|
<ModalFooter>
|
|
<Button variant="default" onClick={() => setDeleteTarget(null)}>Cancel</Button>
|
|
<Button color="red" onClick={confirmDelete}>Delete Field</Button>
|
|
</ModalFooter>
|
|
</Stack>
|
|
</Modal>
|
|
</Stack>
|
|
);
|
|
}
|