277 lines
11 KiB
TypeScript
277 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
|
|
import {
|
|
PlusIcon,
|
|
MagnifyingGlassIcon,
|
|
PencilSimpleLineIcon,
|
|
TrashIcon,
|
|
DotsSixVerticalIcon,
|
|
} from '@phosphor-icons/react';
|
|
import { CustomInput, CustomButton, Skeleton } from '../../../../components/custom';
|
|
import type { FieldDefinition } from '../ActionBuilderTypes';
|
|
|
|
interface ConfigurationFieldsColumnProps {
|
|
fields: FieldDefinition[];
|
|
selectedActionTypeId: string;
|
|
loading: boolean;
|
|
onOpenAdd: () => void;
|
|
onOpenEdit: (field: FieldDefinition) => void;
|
|
onDelete: (field: FieldDefinition) => void;
|
|
onReorderFields?: (fromIndex: number, toIndex: number) => void;
|
|
}
|
|
|
|
export function ConfigurationFieldsColumn({
|
|
fields,
|
|
selectedActionTypeId,
|
|
loading,
|
|
onOpenAdd,
|
|
onOpenEdit,
|
|
onDelete,
|
|
onReorderFields,
|
|
}: ConfigurationFieldsColumnProps) {
|
|
const [search, setSearch] = useState('');
|
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
|
|
const filteredFields = fields.filter(
|
|
(f) =>
|
|
f.fieldName.toLowerCase().includes(search.toLowerCase()) ||
|
|
f.fieldCode.toLowerCase().includes(search.toLowerCase()) ||
|
|
(f.section || '').toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
|
|
const fieldNameByCode = new Map(fields.map((f) => [f.fieldCode, f.fieldName]));
|
|
|
|
return (
|
|
<div className="bg-[#FFFFFF] p-4 rounded-[20px] border border-gray-200/70 shadow-sm space-y-4 flex flex-col min-h-[600px]">
|
|
{/* Column Header */}
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-[16px] font-bold text-[#0F172B]">Configuration Field</h3>
|
|
<CustomButton
|
|
variant="outlined"
|
|
size="sm"
|
|
onClick={onOpenAdd}
|
|
disabled={!selectedActionTypeId}
|
|
leftIcon={<PlusIcon size={15} weight="bold" />}
|
|
>
|
|
Add
|
|
</CustomButton>
|
|
</div>
|
|
|
|
{/* Search Input */}
|
|
<div className="relative">
|
|
<CustomInput
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Search by name or code..."
|
|
leftIcon={<MagnifyingGlassIcon size={16} className="text-slate-400" />}
|
|
className="!bg-[#F3F6F5] !rounded-[12px] !h-[40px] !border-none !text-[13px]"
|
|
containerClassName="!gap-0"
|
|
/>
|
|
</div>
|
|
|
|
{/* Field Cards List */}
|
|
<div className="space-y-3 flex-1 overflow-y-auto max-h-[580px] pr-0.5">
|
|
{loading ? (
|
|
<div className="flex flex-col gap-3">
|
|
<Skeleton width="100%" height={100} className="rounded-[16px]" />
|
|
<Skeleton width="100%" height={100} className="rounded-[16px]" />
|
|
<Skeleton width="100%" height={100} className="rounded-[16px]" />
|
|
</div>
|
|
) : !selectedActionTypeId ? (
|
|
<div className="py-12 text-center text-slate-400 text-[13px]">
|
|
Select an Action Type to view configuration fields.
|
|
</div>
|
|
) : filteredFields.length === 0 ? (
|
|
<div className="py-12 text-center text-slate-400 text-[13px]">
|
|
No fields configured for this action type yet. Click "+ Add" above to create one.
|
|
</div>
|
|
) : (
|
|
filteredFields.map((f, idx) => {
|
|
const hasVisibility = f.visibilityConditionJson?.field && f.visibilityConditionJson?.value;
|
|
const depFieldName = hasVisibility
|
|
? fieldNameByCode.get(f.visibilityConditionJson!.field) || f.visibilityConditionJson!.field
|
|
: 'Field Name';
|
|
|
|
const minVal = f.validationJson?.min;
|
|
const maxVal = f.validationJson?.max;
|
|
const hasMinMax = minVal !== undefined || maxVal !== undefined;
|
|
|
|
|
|
const isDragging = draggedIndex === idx;
|
|
const isDragOver = dragOverIndex === idx;
|
|
|
|
return (
|
|
<div
|
|
key={f.id}
|
|
draggable
|
|
onDragStart={(e) => {
|
|
setDraggedIndex(idx);
|
|
e.dataTransfer.setData('text/plain', String(idx));
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
}}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
if (dragOverIndex !== idx) {
|
|
setDragOverIndex(idx);
|
|
}
|
|
}}
|
|
onDragLeave={() => {
|
|
if (dragOverIndex === idx) {
|
|
setDragOverIndex(null);
|
|
}
|
|
}}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
if (draggedIndex !== null && draggedIndex !== idx) {
|
|
const fromOriginalIndex = fields.findIndex((item) => item.id === filteredFields[draggedIndex].id);
|
|
const toOriginalIndex = fields.findIndex((item) => item.id === f.id);
|
|
if (fromOriginalIndex !== -1 && toOriginalIndex !== -1) {
|
|
onReorderFields?.(fromOriginalIndex, toOriginalIndex);
|
|
}
|
|
}
|
|
setDraggedIndex(null);
|
|
setDragOverIndex(null);
|
|
}}
|
|
onDragEnd={() => {
|
|
setDraggedIndex(null);
|
|
setDragOverIndex(null);
|
|
}}
|
|
className={`bg-[#EFF3F7] rounded-[16px] border border-[#E2E8F0] overflow-hidden flex items-stretch transition-all duration-150 ${isDragOver ? 'ring-2 ring-[#1E7D5C] scale-[1.01]' : 'hover:border-slate-300'
|
|
} ${isDragging ? 'opacity-40' : ''}`}
|
|
>
|
|
{/* Left Drag Handle Bar */}
|
|
<div
|
|
className="w-8 flex items-center justify-center text-slate-400 hover:text-slate-700 cursor-grab active:cursor-grabbing flex-shrink-0 select-none"
|
|
title="Drag to reorder"
|
|
>
|
|
<DotsSixVerticalIcon size={18} weight="bold" />
|
|
</div>
|
|
|
|
{/* Inner White Card Content */}
|
|
<div className="bg-white flex-1 p-4 rounded-r-[15px] border-l border-[#E2E8F0] space-y-3 min-w-0">
|
|
{/* Top Header Row */}
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div>
|
|
<div className="font-bold text-[15px] text-[#0F172B] leading-tight">
|
|
{f.fieldName}
|
|
</div>
|
|
<div className="font-mono text-[12px] text-[#8C9AA8] uppercase tracking-wider mt-0.5">
|
|
{f.fieldCode}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{/* Required Badge */}
|
|
{f.isRequired && (
|
|
<span className="bg-[#FEECEB] text-[#D9383A] text-[12px] font-medium px-3 py-1 rounded-full">
|
|
Required
|
|
</span>
|
|
)}
|
|
|
|
{/* Active Badge */}
|
|
<span
|
|
className={`text-[12px] font-medium px-3 py-1 rounded-full ${f.isActive !== false
|
|
? 'bg-[#E6F7ED] text-[#1E7D5C]'
|
|
: 'bg-slate-100 text-slate-500'
|
|
}`}
|
|
>
|
|
{f.isActive !== false ? 'Active' : 'Inactive'}
|
|
</span>
|
|
|
|
{/* Divider */}
|
|
<span className="text-slate-300 font-light mx-0.5">|</span>
|
|
|
|
{/* Action Buttons */}
|
|
<button
|
|
type="button"
|
|
onClick={() => onOpenEdit(f)}
|
|
title="Edit Field"
|
|
className="p-1 text-slate-800 hover:text-[#1E7D5C] transition-colors cursor-pointer"
|
|
>
|
|
<PencilSimpleLineIcon size={18} weight="bold" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onDelete(f)}
|
|
title="Delete Field"
|
|
className="p-1 text-[#D9383A] hover:text-red-700 transition-colors cursor-pointer"
|
|
>
|
|
<TrashIcon size={18} weight="bold" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-b border-slate-100 my-2" />
|
|
|
|
{/* Details Grid (2 columns x 2 rows) */}
|
|
<div className="grid grid-cols-2 gap-y-3 gap-x-4">
|
|
<div>
|
|
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
|
|
CONTROL TYPE
|
|
</div>
|
|
<div className="text-[13px] font-bold text-[#0F172B] mt-0.5">
|
|
{f.fieldType}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
|
|
WIDTH
|
|
</div>
|
|
<div className="text-[13px] font-bold text-[#0F172B] mt-0.5">
|
|
{f.width ? `${f.width} width` : 'full width'}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
|
|
MASTER LOOKUP
|
|
</div>
|
|
<div className="text-[13px] font-bold text-[#0F172B] mt-0.5">
|
|
{f.lookupSource || '--'}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
|
|
SECTION
|
|
</div>
|
|
<div className="text-[13px] font-medium text-[#5A6E85] mt-0.5">
|
|
{f.section || 'General Information'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Visibility Condition Banner & Min/Max Footer */}
|
|
<div className="pt-2 flex items-end justify-between gap-3">
|
|
{hasVisibility ? (
|
|
<div className="bg-[#FFF8E6] border border-[#FDE6B8] text-[#B45309] rounded-[10px] px-3.5 py-2 text-[12px] space-y-0.5 max-w-[70%]">
|
|
<div className="font-medium text-[#B45309]">Visible if {depFieldName} =</div>
|
|
<div className="font-bold text-[#B45309] uppercase break-all">
|
|
{String(f.visibilityConditionJson?.value)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div />
|
|
)}
|
|
|
|
{hasMinMax && (
|
|
<div className="text-right text-[12px] font-medium text-[#5A6E85] ml-auto">
|
|
Min: {minVal ?? '—'} | Max: {maxVal ?? '—'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|