feat: implement ThresholdsBuilder and InstructionsBuilder components for dynamic form logic
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useFieldArray, useWatch, Controller, type Control, type UseFormRegister } from 'react-hook-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 {
|
||||
Select,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { ThresholdsBuilder } from './ThresholdsBuilder';
|
||||
import { CreatableCombobox } from '@/components/ui/creatable-combobox';
|
||||
|
||||
const getAvailableOperations = (type?: string) => {
|
||||
switch (type) {
|
||||
@@ -36,36 +37,49 @@ interface InstructionsBuilderProps {
|
||||
}
|
||||
|
||||
export function InstructionsBuilder({ control, register }: InstructionsBuilderProps) {
|
||||
const currentEventName = useWatch({ control, name: 'name' });
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'instructions',
|
||||
});
|
||||
|
||||
const [schema, setSchema] = useState<Record<string, { code: string; type: string }[]>>({});
|
||||
const [events, setEvents] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/v1/attributes/schema')
|
||||
.then(res => res.json())
|
||||
.then(data => setSchema(data))
|
||||
.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 (
|
||||
<div className="space-y-4">
|
||||
<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
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
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>
|
||||
</div>
|
||||
|
||||
{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">
|
||||
@@ -78,6 +92,7 @@ export function InstructionsBuilder({ control, register }: InstructionsBuilderPr
|
||||
register={register}
|
||||
remove={remove}
|
||||
schema={schema}
|
||||
eventOptions={eventOptions}
|
||||
/>
|
||||
))}
|
||||
</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 currentEntity = useWatch({ control, name: `instructions.${index}.entityType` });
|
||||
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);
|
||||
|
||||
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">
|
||||
<AccordionTrigger className="hover:no-underline flex-1 py-4 font-semibold text-gray-700">
|
||||
{`▼ Instruction ${index + 1}: ${instructionName || 'New Instruction'}`}
|
||||
<AccordionTrigger className="hover:no-underline flex-1 py-4 font-semibold text-gray-800">
|
||||
<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>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
remove(index);
|
||||
@@ -124,20 +151,24 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
|
||||
<Trash2 className="w-4 h-4 mr-1" /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
<AccordionContent className="pb-4 pt-2">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="text-sm font-medium text-gray-700 min-w-[120px]">Instruction Name:</span>
|
||||
<AccordionContent className="pb-5 pt-1">
|
||||
<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-slate-700 min-w-[100px]">Action Name:</span>
|
||||
<Input
|
||||
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 })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<div className="grid grid-cols-3 gap-3 flex-1">
|
||||
<div className="mb-6 rounded-xl border border-slate-200 bg-slate-50/30 overflow-hidden">
|
||||
<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>
|
||||
<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
|
||||
control={control}
|
||||
name={`instructions.${index}.entityType` as const}
|
||||
@@ -150,7 +181,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
|
||||
value={normalizedValue || ""}
|
||||
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" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -165,7 +196,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
|
||||
/>
|
||||
</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
|
||||
control={control}
|
||||
name={`instructions.${index}.attributeCode` as const}
|
||||
@@ -177,7 +208,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
|
||||
disabled={!currentEntity}
|
||||
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" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -192,7 +223,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
|
||||
/>
|
||||
</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
|
||||
control={control}
|
||||
name={`instructions.${index}.operation` as const}
|
||||
@@ -202,7 +233,7 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
|
||||
value={field.value || ""}
|
||||
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" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -217,28 +248,48 @@ function InstructionRow({ fieldId, index, control, register, remove, schema }: a
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DynamicFields control={control} register={register} index={index} />
|
||||
<DynamicFields control={control} register={register} index={index} eventOptions={eventOptions} />
|
||||
|
||||
{currentOperation !== 'SET_TRUE_ONCE' && (
|
||||
<ThresholdsBuilder control={control} register={register} instructionIndex={index} attributeType={attributeType} operation={currentOperation} />
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</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({
|
||||
control,
|
||||
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 (
|
||||
<div className="mb-4 bg-white p-3 rounded border">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">Counter Configuration</h4>
|
||||
<div className="flex gap-4">
|
||||
<Wrapper title="Counter Configuration">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<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
|
||||
control={control}
|
||||
name={`instructions.${index}.periodType` as const}
|
||||
@@ -254,7 +305,7 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
|
||||
YEARLY: 'Yearly'
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-white h-9">
|
||||
<SelectTrigger className="bg-white h-9 shadow-sm">
|
||||
<SelectValue placeholder="Select Period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -268,67 +319,72 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<label className="block text-xs text-gray-500 mb-1">Step</label>
|
||||
<div className="w-full md:w-48">
|
||||
<label className="block text-xs font-medium text-slate-600 mb-1.5">Step</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="e.g. 1"
|
||||
className="bg-white h-9"
|
||||
className="bg-white h-9 shadow-sm"
|
||||
{...register(`instructions.${index}.step` as const, { valueAsNumber: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'APPEND_UNIQUE') {
|
||||
return (
|
||||
<div className="mb-4 bg-white p-3 rounded border">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">Value Mapping</h4>
|
||||
<div className="flex gap-4">
|
||||
<Wrapper title="Value Mapping">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<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
|
||||
placeholder="e.g. event.itemId"
|
||||
className="bg-white h-9"
|
||||
className="bg-white h-9 shadow-sm"
|
||||
{...register(`instructions.${index}.sourceField` as const)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<label className="block text-xs text-gray-500 mb-1">Max Length</label>
|
||||
<div className="w-full md:w-48">
|
||||
<label className="block text-xs font-medium text-slate-600 mb-1.5">Max Length</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="e.g. 100"
|
||||
className="bg-white h-9"
|
||||
className="bg-white h-9 shadow-sm"
|
||||
{...register(`instructions.${index}.maxLength` as const, { valueAsNumber: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'SET_TRUE_ONCE') {
|
||||
return (
|
||||
<div className="mb-4 bg-white p-3 rounded border">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">One-Time Transition Trigger</h4>
|
||||
<Wrapper title="One-Time Transition Trigger">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-gray-500 mb-1">Trigger Event (on False → True)</label>
|
||||
<Input
|
||||
placeholder="e.g. First_Login_Reward"
|
||||
className="bg-white h-9"
|
||||
{...register(`instructions.${index}.triggerEventOnSuccess` as const)}
|
||||
<label className="block text-xs font-medium text-slate-600 mb-1.5">Trigger Event (on False → True)</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`instructions.${index}.triggerEventOnSuccess`}
|
||||
render={({ field }) => (
|
||||
<CreatableCombobox
|
||||
options={eventOptions}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="e.g. First_Login_Reward"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'COMPUTE_GAP' || operation === 'CHECK_AND_UPDATE_TIMER') {
|
||||
return (
|
||||
<div className="mb-4 bg-white p-3 rounded border">
|
||||
<label className="block text-xs text-gray-500 mb-1">Unit</label>
|
||||
<Wrapper title="Unit Configuration">
|
||||
<label className="block text-xs font-medium text-slate-600 mb-1.5">Unit</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`instructions.${index}.unit` as const}
|
||||
@@ -342,7 +398,7 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
|
||||
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" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -353,7 +409,7 @@ function DynamicFields({ control, register, index }: { control: Control<any>, re
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Settings2 } from 'lucide-react';
|
||||
|
||||
const getAvailableConditions = (type?: string) => {
|
||||
switch (type) {
|
||||
@@ -65,138 +66,144 @@ export function ThresholdsBuilder({ control, register, instructionIndex, attribu
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-3 mt-4 border-l-2 border-blue-200 pl-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-gray-700">Thresholds Config</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
className="h-8 text-xs bg-white"
|
||||
onClick={() => append({ operator: defaultOperator, value: defaultValue, triggerEvent: '', resetPolicy: 'NONE' })}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" /> Add Threshold
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col gap-4 bg-white p-4 rounded border border-gray-200">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700 w-24">Condition:</span>
|
||||
<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">
|
||||
{/* Condition Group */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-xs font-medium text-slate-500 w-16">IF Value</span>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`instructions.${instructionIndex}.thresholds.${index}.operator` as const}
|
||||
defaultValue={defaultOperator}
|
||||
render={({ field }) => {
|
||||
const currentValue = availableConditions.find(c => c.value === field.value) ? field.value : defaultOperator;
|
||||
return (
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={currentValue}
|
||||
items={availableConditions.reduce((acc, c) => ({ ...acc, [c.value]: c.label }), {})}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-50 h-8 w-[70px] px-2 text-xs border-slate-200 shadow-none">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableConditions.map(c => (
|
||||
<SelectItem key={c.value} value={c.value} label={c.label}>{c.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}}
|
||||
/>
|
||||
{attributeType === 'BOOLEAN' ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`instructions.${instructionIndex}.thresholds.${index}.operator` as const}
|
||||
defaultValue={defaultOperator}
|
||||
render={({ field }) => {
|
||||
const currentValue = availableConditions.find(c => c.value === field.value) ? field.value : defaultOperator;
|
||||
return (
|
||||
name={`instructions.${instructionIndex}.thresholds.${index}.value` as const}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={currentValue}
|
||||
items={availableConditions.reduce((acc, c) => ({ ...acc, [c.value]: c.label }), {})}
|
||||
onValueChange={(val) => field.onChange(val === 'true')}
|
||||
value={field.value !== undefined ? String(field.value) : ""}
|
||||
items={{
|
||||
'true': 'True',
|
||||
'false': 'False'
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-white h-9 w-[70px] px-2 text-sm">
|
||||
<SelectValue />
|
||||
<SelectTrigger className="bg-slate-50 h-8 w-[80px] text-xs border-slate-200 shadow-none">
|
||||
<SelectValue placeholder="Value" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableConditions.map(c => (
|
||||
<SelectItem key={c.value} value={c.value} label={c.label}>{c.label}</SelectItem>
|
||||
))}
|
||||
<SelectItem value="true" label="True">True</SelectItem>
|
||||
<SelectItem value="false" label="False">False</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}}
|
||||
)}
|
||||
/>
|
||||
{attributeType === 'BOOLEAN' ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`instructions.${instructionIndex}.thresholds.${index}.value` as const}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
onValueChange={(val) => field.onChange(val === 'true')}
|
||||
value={field.value !== undefined ? String(field.value) : ""}
|
||||
items={{
|
||||
'true': 'True',
|
||||
'false': 'False'
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-white h-9 w-24">
|
||||
<SelectValue placeholder="Value" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true" label="True">True</SelectItem>
|
||||
<SelectItem value="false" label="False">False</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type={attributeType === 'STRING' ? 'text' : 'number'}
|
||||
placeholder="Value"
|
||||
className="h-9 w-24"
|
||||
{...register(`instructions.${instructionIndex}.thresholds.${index}.value` as const, { required: true, valueAsNumber: attributeType === 'NUMBER' })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-1 min-w-[250px] ml-4">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">Trigger: Fire Event</span>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`instructions.${instructionIndex}.thresholds.${index}.triggerEvent`}
|
||||
render={({ field }) => (
|
||||
) : (
|
||||
<Input
|
||||
type={attributeType === 'STRING' ? 'text' : 'number'}
|
||||
placeholder="Value"
|
||||
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' })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trigger Group */}
|
||||
<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">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`instructions.${instructionIndex}.thresholds.${index}.triggerEvent`}
|
||||
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
|
||||
options={eventOptions}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="e.g. milestone_reached"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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
|
||||
type="button"
|
||||
variant="ghost"
|
||||
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)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user