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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user