feat: implement ThresholdsBuilder and InstructionsBuilder components for dynamic form logic

This commit is contained in:
SonPhung
2026-07-15 01:15:14 +07:00
parent 19087ec1af
commit f33a1e05ad
2 changed files with 213 additions and 150 deletions

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useFieldArray, useWatch, Controller, type Control, type UseFormRegister } from 'react-hook-form'; import { useFieldArray, useWatch, Controller, type Control, type UseFormRegister } from 'react-hook-form';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Plus, Trash2 } from 'lucide-react'; import { Plus, Trash2, Target, Settings2, Zap, ArrowRight } from 'lucide-react';
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'; import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
import { import {
Select, Select,
@@ -12,6 +12,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { ThresholdsBuilder } from './ThresholdsBuilder'; import { ThresholdsBuilder } from './ThresholdsBuilder';
import { CreatableCombobox } from '@/components/ui/creatable-combobox';
const getAvailableOperations = (type?: string) => { const getAvailableOperations = (type?: string) => {
switch (type) { switch (type) {
@@ -36,36 +37,49 @@ interface InstructionsBuilderProps {
} }
export function InstructionsBuilder({ control, register }: InstructionsBuilderProps) { export function InstructionsBuilder({ control, register }: InstructionsBuilderProps) {
const currentEventName = useWatch({ control, name: 'name' });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
name: 'instructions', name: 'instructions',
}); });
const [schema, setSchema] = useState<Record<string, { code: string; type: string }[]>>({}); const [schema, setSchema] = useState<Record<string, { code: string; type: string }[]>>({});
const [events, setEvents] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
fetch('/api/v1/attributes/schema') fetch('/api/v1/attributes/schema')
.then(res => res.json()) .then(res => res.json())
.then(data => setSchema(data)) .then(data => setSchema(data))
.catch(console.error); .catch(console.error);
fetch('/api/v1/events')
.then(res => res.json())
.then(data => {
if (Array.isArray(data)) {
setEvents(Array.from(new Set(data.map((e: any) => e.name))));
}
})
.catch(console.error);
}, []); }, []);
const eventOptions = events.filter(e => e !== currentEventName);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-medium">Counter Instructions</h3> <h3 className="text-lg font-medium">Counter Actions</h3>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => append({ name: '', entityType: '', attributeCode: '', operation: 'INCREMENT', thresholds: [] })} onClick={() => append({ name: '', entityType: '', attributeCode: '', operation: 'INCREMENT', thresholds: [] })}
> >
<Plus className="w-4 h-4 mr-2" /> Add Instruction <Plus className="w-4 h-4 mr-2" /> Add Action
</Button> </Button>
</div> </div>
{fields.length === 0 && ( {fields.length === 0 && (
<p className="text-sm text-gray-500">No instructions defined.</p> <p className="text-sm text-gray-500">No actions defined.</p>
)} )}
<Accordion type="multiple" className="w-full space-y-4"> <Accordion type="multiple" className="w-full space-y-4">
@@ -78,6 +92,7 @@ export function InstructionsBuilder({ control, register }: InstructionsBuilderPr
register={register} register={register}
remove={remove} remove={remove}
schema={schema} schema={schema}
eventOptions={eventOptions}
/> />
))} ))}
</Accordion> </Accordion>
@@ -85,7 +100,7 @@ export function InstructionsBuilder({ control, register }: InstructionsBuilderPr
); );
} }
function InstructionRow({ fieldId, index, control, register, remove, schema }: any) { function InstructionRow({ fieldId, index, control, register, remove, schema, eventOptions }: any) {
const instructionName = useWatch({ control, name: `instructions.${index}.name` }); const instructionName = useWatch({ control, name: `instructions.${index}.name` });
const currentEntity = useWatch({ control, name: `instructions.${index}.entityType` }); const currentEntity = useWatch({ control, name: `instructions.${index}.entityType` });
const currentAttributeCode = useWatch({ control, name: `instructions.${index}.attributeCode` }); const currentAttributeCode = useWatch({ control, name: `instructions.${index}.attributeCode` });
@@ -106,16 +121,28 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
const availableOperations = getAvailableOperations(attributeType); const availableOperations = getAvailableOperations(attributeType);
return ( return (
<AccordionItem value={fieldId} className="bg-gray-50 border rounded-md px-4"> <AccordionItem value={fieldId} className="bg-white border rounded-xl shadow-sm px-4 overflow-hidden">
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<AccordionTrigger className="hover:no-underline flex-1 py-4 font-semibold text-gray-700"> <AccordionTrigger className="hover:no-underline flex-1 py-4 font-semibold text-gray-800">
{`▼ Instruction ${index + 1}: ${instructionName || 'New Instruction'}`} <div className="flex flex-wrap items-center gap-3 text-left">
<span>{`Action ${index + 1}: ${instructionName || 'New Action'}`}</span>
{currentEntity && currentAttributeCode && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded-md text-[11px] font-medium border border-blue-100 flex items-center gap-1 font-normal">
{currentEntity} <ArrowRight className="w-3 h-3" /> {currentAttributeCode}
</span>
)}
{currentOperation && (
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded-md text-[11px] font-medium border border-slate-200 font-normal">
{availableOperations.find(op => op.value === currentOperation)?.label || currentOperation}
</span>
)}
</div>
</AccordionTrigger> </AccordionTrigger>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-gray-500 hover:text-red-600 hover:bg-red-50 ml-4" className="text-gray-500 hover:text-red-600 hover:bg-red-50 ml-4 shrink-0"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
remove(index); remove(index);
@@ -124,20 +151,24 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
<Trash2 className="w-4 h-4 mr-1" /> Delete <Trash2 className="w-4 h-4 mr-1" /> Delete
</Button> </Button>
</div> </div>
<AccordionContent className="pb-4 pt-2"> <AccordionContent className="pb-5 pt-1">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-5 bg-slate-50/50 p-3 rounded-lg border border-slate-100">
<span className="text-sm font-medium text-gray-700 min-w-[120px]">Instruction Name:</span> <span className="text-sm font-medium text-slate-700 min-w-[100px]">Action Name:</span>
<Input <Input
placeholder="e.g. Counter Lượt Xem Tuần" placeholder="e.g. Counter Lượt Xem Tuần"
className="bg-white h-9 flex-1" className="bg-white h-9 flex-1 shadow-sm"
{...register(`instructions.${index}.name` as const, { required: true })} {...register(`instructions.${index}.name` as const, { required: true })}
/> />
</div> </div>
<div className="flex items-start justify-between gap-4 mb-4"> <div className="mb-6 rounded-xl border border-slate-200 bg-slate-50/30 overflow-hidden">
<div className="grid grid-cols-3 gap-3 flex-1"> <div className="bg-slate-100/50 px-4 py-2.5 border-b border-slate-200 flex items-center gap-2">
<Target className="w-4 h-4 text-blue-600" />
<h4 className="text-sm font-semibold text-slate-700">1. Target Definition</h4>
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label className="block text-xs font-medium text-gray-500 mb-1">Target Entity</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Target Entity</label>
<Controller <Controller
control={control} control={control}
name={`instructions.${index}.entityType` as const} name={`instructions.${index}.entityType` as const}
@@ -150,7 +181,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
value={normalizedValue || ""} value={normalizedValue || ""}
items={entities.reduce((acc, e) => ({ ...acc, [e]: e.charAt(0).toUpperCase() + e.slice(1).toLowerCase() }), {})} items={entities.reduce((acc, e) => ({ ...acc, [e]: e.charAt(0).toUpperCase() + e.slice(1).toLowerCase() }), {})}
> >
<SelectTrigger className="bg-white h-9"> <SelectTrigger className="bg-white h-9 shadow-sm">
<SelectValue placeholder="Select Entity" /> <SelectValue placeholder="Select Entity" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -165,7 +196,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-500 mb-1">Target Attribute</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Target Attribute</label>
<Controller <Controller
control={control} control={control}
name={`instructions.${index}.attributeCode` as const} name={`instructions.${index}.attributeCode` as const}
@@ -177,7 +208,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
disabled={!currentEntity} disabled={!currentEntity}
items={availableAttributes.reduce((acc: any, attr: any) => ({ ...acc, [attr.code]: attr.code }), {})} items={availableAttributes.reduce((acc: any, attr: any) => ({ ...acc, [attr.code]: attr.code }), {})}
> >
<SelectTrigger className="bg-white h-9"> <SelectTrigger className="bg-white h-9 shadow-sm">
<SelectValue placeholder="Select Attribute" /> <SelectValue placeholder="Select Attribute" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -192,7 +223,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-500 mb-1">Operation</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Operation</label>
<Controller <Controller
control={control} control={control}
name={`instructions.${index}.operation` as const} name={`instructions.${index}.operation` as const}
@@ -202,7 +233,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
value={field.value || ""} value={field.value || ""}
items={availableOperations.reduce((acc, op) => ({ ...acc, [op.value]: op.label }), {})} items={availableOperations.reduce((acc, op) => ({ ...acc, [op.value]: op.label }), {})}
> >
<SelectTrigger className="bg-white h-9"> <SelectTrigger className="bg-white h-9 shadow-sm">
<SelectValue placeholder="Select Operation" /> <SelectValue placeholder="Select Operation" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -217,28 +248,48 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
</div> </div>
</div> </div>
<DynamicFields control={control} register={register} index={index} /> <DynamicFields control={control} register={register} index={index} eventOptions={eventOptions} />
{currentOperation !== 'SET_TRUE_ONCE' && ( {currentOperation !== 'SET_TRUE_ONCE' && (
<div className="mt-6 rounded-xl border border-dashed border-slate-300 bg-white overflow-hidden shadow-sm">
<div className="bg-slate-50/80 px-4 py-2.5 border-b border-dashed border-slate-300 flex items-center gap-2">
<Zap className="w-4 h-4 text-amber-500" />
<h4 className="text-sm font-semibold text-slate-700">3. Thresholds Config</h4>
</div>
<div className="p-4">
<ThresholdsBuilder control={control} register={register} instructionIndex={index} attributeType={attributeType} operation={currentOperation} /> <ThresholdsBuilder control={control} register={register} instructionIndex={index} attributeType={attributeType} operation={currentOperation} />
</div>
</div>
)} )}
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
); );
} }
function DynamicFields({ control, register, index }: { control: Control<any>, register: UseFormRegister<any>, index: number }) { function DynamicFields({ control, register, index, eventOptions }: { control: Control<any>, register: UseFormRegister<any>, index: number, eventOptions: string[] }) {
const operation = useWatch({ const operation = useWatch({
control, control,
name: `instructions.${index}.operation` name: `instructions.${index}.operation`
}); });
if (operation === 'INCREMENT' || operation === 'DECREMENT') { const Wrapper = ({ title, children }: { title: string, children: React.ReactNode }) => (
<div className="mb-6 rounded-xl border border-slate-200 bg-white overflow-hidden shadow-sm">
<div className="bg-slate-50/80 px-4 py-2.5 border-b border-slate-200 flex items-center gap-2">
<Settings2 className="w-4 h-4 text-slate-500" />
<h4 className="text-sm font-semibold text-slate-700">2. {title}</h4>
</div>
<div className="p-4">
{children}
</div>
</div>
);
if (operation === 'INCREMENT' || operation === 'DECREMENT' || operation === 'STREAK_INCREMENT') {
return ( return (
<div className="mb-4 bg-white p-3 rounded border"> <Wrapper title="Counter Configuration">
<h4 className="text-sm font-medium text-gray-700 mb-3">Counter Configuration</h4> <div className="flex flex-col md:flex-row gap-4">
<div className="flex gap-4">
<div className="flex-1"> <div className="flex-1">
<label className="block text-xs text-gray-500 mb-1">Period Type (Lazy Reset)</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Period Type (Lazy Reset)</label>
<Controller <Controller
control={control} control={control}
name={`instructions.${index}.periodType` as const} name={`instructions.${index}.periodType` as const}
@@ -254,7 +305,7 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
YEARLY: 'Yearly' YEARLY: 'Yearly'
}} }}
> >
<SelectTrigger className="bg-white h-9"> <SelectTrigger className="bg-white h-9 shadow-sm">
<SelectValue placeholder="Select Period" /> <SelectValue placeholder="Select Period" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -268,67 +319,72 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
)} )}
/> />
</div> </div>
<div className="w-48"> <div className="w-full md:w-48">
<label className="block text-xs text-gray-500 mb-1">Step</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Step</label>
<Input <Input
type="number" type="number"
placeholder="e.g. 1" placeholder="e.g. 1"
className="bg-white h-9" className="bg-white h-9 shadow-sm"
{...register(`instructions.${index}.step` as const, { valueAsNumber: true })} {...register(`instructions.${index}.step` as const, { valueAsNumber: true })}
/> />
</div> </div>
</div> </div>
</div> </Wrapper>
); );
} }
if (operation === 'APPEND_UNIQUE') { if (operation === 'APPEND_UNIQUE') {
return ( return (
<div className="mb-4 bg-white p-3 rounded border"> <Wrapper title="Value Mapping">
<h4 className="text-sm font-medium text-gray-700 mb-3">Value Mapping</h4> <div className="flex flex-col md:flex-row gap-4">
<div className="flex gap-4">
<div className="flex-1"> <div className="flex-1">
<label className="block text-xs text-gray-500 mb-1">Source Field</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Source Field</label>
<Input <Input
placeholder="e.g. event.itemId" placeholder="e.g. event.itemId"
className="bg-white h-9" className="bg-white h-9 shadow-sm"
{...register(`instructions.${index}.sourceField` as const)} {...register(`instructions.${index}.sourceField` as const)}
/> />
</div> </div>
<div className="w-48"> <div className="w-full md:w-48">
<label className="block text-xs text-gray-500 mb-1">Max Length</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Max Length</label>
<Input <Input
type="number" type="number"
placeholder="e.g. 100" placeholder="e.g. 100"
className="bg-white h-9" className="bg-white h-9 shadow-sm"
{...register(`instructions.${index}.maxLength` as const, { valueAsNumber: true })} {...register(`instructions.${index}.maxLength` as const, { valueAsNumber: true })}
/> />
</div> </div>
</div> </div>
</div> </Wrapper>
); );
} }
if (operation === 'SET_TRUE_ONCE') { if (operation === 'SET_TRUE_ONCE') {
return ( return (
<div className="mb-4 bg-white p-3 rounded border"> <Wrapper title="One-Time Transition Trigger">
<h4 className="text-sm font-medium text-gray-700 mb-3">One-Time Transition Trigger</h4>
<div className="flex-1"> <div className="flex-1">
<label className="block text-xs text-gray-500 mb-1">Trigger Event (on False True)</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Trigger Event (on False True)</label>
<Input <Controller
control={control}
name={`instructions.${index}.triggerEventOnSuccess`}
render={({ field }) => (
<CreatableCombobox
options={eventOptions}
value={field.value}
onChange={field.onChange}
placeholder="e.g. First_Login_Reward" placeholder="e.g. First_Login_Reward"
className="bg-white h-9" />
{...register(`instructions.${index}.triggerEventOnSuccess` as const)} )}
/> />
</div> </div>
</div> </Wrapper>
); );
} }
if (operation === 'COMPUTE_GAP' || operation === 'CHECK_AND_UPDATE_TIMER') { if (operation === 'COMPUTE_GAP' || operation === 'CHECK_AND_UPDATE_TIMER') {
return ( return (
<div className="mb-4 bg-white p-3 rounded border"> <Wrapper title="Unit Configuration">
<label className="block text-xs text-gray-500 mb-1">Unit</label> <label className="block text-xs font-medium text-slate-600 mb-1.5">Unit</label>
<Controller <Controller
control={control} control={control}
name={`instructions.${index}.unit` as const} name={`instructions.${index}.unit` as const}
@@ -342,7 +398,7 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
MINUTES: 'Minutes' MINUTES: 'Minutes'
}} }}
> >
<SelectTrigger className="bg-white h-9 w-48"> <SelectTrigger className="bg-white h-9 w-full md:w-48 shadow-sm">
<SelectValue placeholder="Select Unit" /> <SelectValue placeholder="Select Unit" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -353,7 +409,7 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
</Select> </Select>
)} )}
/> />
</div> </Wrapper>
); );
} }

View File

@@ -14,6 +14,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Settings2 } from 'lucide-react';
const getAvailableConditions = (type?: string) => { const getAvailableConditions = (type?: string) => {
switch (type) { switch (type) {
@@ -65,26 +66,25 @@ export function ThresholdsBuilder({ control, register, instructionIndex, attribu
}, []); }, []);
return ( return (
<div className="space-y-3 mt-4 border-l-2 border-blue-200 pl-4"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-end">
<h4 className="text-sm font-medium text-gray-700">Thresholds Config</h4>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
className="h-8 text-xs" className="h-8 text-xs bg-white"
onClick={() => append({ operator: defaultOperator, value: defaultValue, triggerEvent: '', resetPolicy: 'NONE' })} onClick={() => append({ operator: defaultOperator, value: defaultValue, triggerEvent: '', resetPolicy: 'NONE' })}
> >
<Plus className="w-3 h-3 mr-1" /> Add Threshold <Plus className="w-3 h-3 mr-1" /> Add Threshold
</Button> </Button>
</div> </div>
<div className="space-y-4"> <div className="space-y-2">
{fields.map((field, index) => ( {fields.map((field, index) => (
<div key={field.id} className="flex flex-col gap-4 bg-white p-4 rounded border border-gray-200"> <div key={field.id} className="flex flex-col xl:flex-row items-start xl:items-center gap-3 bg-white p-3 rounded-lg border border-slate-200 shadow-sm transition-all hover:border-blue-200">
<div className="flex items-center gap-6"> {/* Condition Group */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-2 shrink-0">
<span className="text-sm font-medium text-gray-700 w-24">Condition:</span> <span className="text-xs font-medium text-slate-500 w-16">IF Value</span>
<Controller <Controller
control={control} control={control}
name={`instructions.${instructionIndex}.thresholds.${index}.operator` as const} name={`instructions.${instructionIndex}.thresholds.${index}.operator` as const}
@@ -97,7 +97,7 @@ export function ThresholdsBuilder({ control, register, instructionIndex, attribu
value={currentValue} value={currentValue}
items={availableConditions.reduce((acc, c) => ({ ...acc, [c.value]: c.label }), {})} items={availableConditions.reduce((acc, c) => ({ ...acc, [c.value]: c.label }), {})}
> >
<SelectTrigger className="bg-white h-9 w-[70px] px-2 text-sm"> <SelectTrigger className="bg-slate-50 h-8 w-[70px] px-2 text-xs border-slate-200 shadow-none">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -122,7 +122,7 @@ export function ThresholdsBuilder({ control, register, instructionIndex, attribu
'false': 'False' 'false': 'False'
}} }}
> >
<SelectTrigger className="bg-white h-9 w-24"> <SelectTrigger className="bg-slate-50 h-8 w-[80px] text-xs border-slate-200 shadow-none">
<SelectValue placeholder="Value" /> <SelectValue placeholder="Value" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -136,67 +136,74 @@ export function ThresholdsBuilder({ control, register, instructionIndex, attribu
<Input <Input
type={attributeType === 'STRING' ? 'text' : 'number'} type={attributeType === 'STRING' ? 'text' : 'number'}
placeholder="Value" placeholder="Value"
className="h-9 w-24" className="h-8 w-[80px] text-xs bg-slate-50 border-slate-200 shadow-none"
{...register(`instructions.${instructionIndex}.thresholds.${index}.value` as const, { required: true, valueAsNumber: attributeType === 'NUMBER' })} {...register(`instructions.${instructionIndex}.thresholds.${index}.value` as const, { required: true, valueAsNumber: attributeType === 'NUMBER' })}
/> />
)} )}
</div> </div>
<div className="flex items-center gap-3 flex-1 min-w-[250px] ml-4"> {/* Trigger Group */}
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">Trigger: Fire Event</span> <div className="flex items-center gap-2 flex-1 w-full xl:w-auto xl:min-w-[200px]">
<span className="text-xs font-medium text-slate-500 xl:ml-2">THEN Trigger</span>
<div className="flex-1"> <div className="flex-1">
<Controller <Controller
control={control} control={control}
name={`instructions.${instructionIndex}.thresholds.${index}.triggerEvent`} name={`instructions.${instructionIndex}.thresholds.${index}.triggerEvent`}
render={({ field }) => ( render={({ field }) => (
<div className="[&_[role=combobox]]:h-8 [&_[role=combobox]]:text-xs [&_[role=combobox]]:bg-slate-50 [&_[role=combobox]]:border-slate-200 [&_[role=combobox]]:shadow-none">
<CreatableCombobox <CreatableCombobox
options={eventOptions} options={eventOptions}
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
placeholder="e.g. milestone_reached" placeholder="e.g. milestone_reached"
/> />
</div>
)} )}
/> />
</div> </div>
</div> </div>
{/* Reset Policy Group */}
{showResetPolicy && (
<div className="flex items-center gap-2 shrink-0 w-full xl:w-auto">
<span className="text-xs font-medium text-slate-500 xl:ml-2">WITH Reset</span>
<Controller
control={control}
name={`instructions.${instructionIndex}.thresholds.${index}.resetPolicy`}
render={({ field }) => (
<Select
onValueChange={field.onChange}
value={field.value || "NONE"}
items={{
'NONE': 'None',
'RESET_ON_TRIGGER': 'Reset on Trigger'
}}
>
<SelectTrigger className="bg-slate-50 h-8 w-[140px] text-xs border-slate-200 shadow-none">
<SelectValue placeholder="Policy" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NONE" label="None">None</SelectItem>
<SelectItem value="RESET_ON_TRIGGER" label="Reset on Trigger">Reset on Trigger</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
)}
{/* Actions */}
<div className="flex items-center justify-end w-full xl:w-auto mt-2 xl:mt-0">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-9 w-9 text-red-500 hover:text-red-700 hover:bg-red-50 ml-auto" className="h-8 w-8 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-md"
onClick={() => remove(index)} onClick={() => remove(index)}
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>
{showResetPolicy && (
<div className="flex items-center gap-6 pt-3 mt-2 border-t border-gray-100">
<span className="text-sm font-medium text-gray-700 w-24">Reset Policy:</span>
<Controller
control={control}
name={`instructions.${instructionIndex}.thresholds.${index}.resetPolicy`}
render={({ field }) => (
<RadioGroup
onValueChange={field.onChange}
value={field.value || ""}
className="flex flex-wrap gap-6"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="NONE" id={`r-none-${index}`} />
<Label htmlFor={`r-none-${index}`} className="cursor-pointer text-sm font-normal text-gray-600">NONE</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="RESET_ON_TRIGGER" id={`r-reset-${index}`} />
<Label htmlFor={`r-reset-${index}`} className="cursor-pointer text-sm font-normal text-gray-600">RESET_ON_TRIGGER</Label>
</div>
</RadioGroup>
)}
/>
</div>
)}
</div> </div>
))} ))}
</div> </div>