add feature dynamic error multitab & continue dict test calc

This commit is contained in:
faiztyanirh 2026-03-04 15:32:57 +07:00
parent d3099248b2
commit c5d2aa6711
4 changed files with 666 additions and 642 deletions

View File

@ -1,10 +1,24 @@
export function useDictionaryForm(formState) { // export function useDictionaryForm(formState) {
let uploadErrors = $state({}); // let uploadErrors = $state({});
let isChecking = $state({}); // let isChecking = $state({});
let hasErrors = $derived( // let hasErrors = $derived(
Object.values(formState.errors).some(value => value !== null) // Object.values(formState.errors).some(value => value !== null)
// );
// return {
// get hasErrors() { return hasErrors },
// }
// }
export function useDictionaryForm(formState, getActiveFormStates) {
let hasErrors = $derived.by(() => {
const mainHasError = Object.values(formState.errors).some(v => v !== null);
const activeHasError = getActiveFormStates().some(fs =>
Object.values(fs.errors).some(v => v !== null)
); );
return mainHasError || activeHasError;
});
return { return {
get hasErrors() { return hasErrors }, get hasErrors() { return hasErrors },

View File

@ -44,6 +44,7 @@ export const testCalSchema = z.object({
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: `Formula must contain all selected input parameters:${data.FormulaInput.join(',')}`, message: `Formula must contain all selected input parameters:${data.FormulaInput.join(',')}`,
// message: `Formula must contain all selected input parameters:`,
path: ['FormulaCode'] path: ['FormulaCode']
}); });
} }

View File

@ -55,7 +55,24 @@
modeOpt: 'cascade' modeOpt: 'cascade'
}) })
const helpers = useDictionaryForm(formState); const activeFormStates = $derived.by(() => {
switch (formState.form.TestType) {
case "TEST":
case "PARAM":
return [refNumState, refTxtState, mapFormState];
case "CALC":
return [calFormState, refNumState, refTxtState, mapFormState];
case "GROUP":
return [];
case "TITLE":
return [];
default:
return [];
}
});
// const helpers = useDictionaryForm(formState);
const helpers = useDictionaryForm(formState, () => activeFormStates);
const handlers = { const handlers = {
clearForm: () => { clearForm: () => {
@ -269,18 +286,6 @@
})); }));
}); });
const activeFormState = $derived.by(() => {
switch (formState.form.TestType) {
case "TEST":
return formState;
case "PARAM":
return formState;
case "CALC":
return calFormState;
default:
return null;
}
});
// $inspect(activeFormState.errors) // $inspect(activeFormState.errors)
let activeTab = $state('definition'); let activeTab = $state('definition');
@ -391,6 +396,11 @@
refTxtState.form.TxtRefType = value; refTxtState.form.TxtRefType = value;
} }
} }
// $inspect({
// definition: formState.errors,
// active: activeFormStates.map(fs => fs.errors)
// });
</script> </script>
<FormPageContainer title="Create Test" {primaryAction} {secondaryActions} {actions}> <FormPageContainer title="Create Test" {primaryAction} {secondaryActions} {actions}>

View File

@ -1,20 +1,20 @@
<script> <script>
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Select from "$lib/components/ui/select/index.js"; import * as Select from '$lib/components/ui/select/index.js';
import { Toggle } from "$lib/components/ui/toggle/index.js"; import { Toggle } from '$lib/components/ui/toggle/index.js';
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from '$lib/components/ui/button/index.js';
import { Input } from "$lib/components/ui/input/index.js"; import { Input } from '$lib/components/ui/input/index.js';
import { Label } from "$lib/components/ui/label/index.js"; import { Label } from '$lib/components/ui/label/index.js';
import { Spinner } from "$lib/components/ui/spinner/index.js"; import { Spinner } from '$lib/components/ui/spinner/index.js';
import ReusableCalendar from "$lib/components/reusable/reusable-calendar.svelte"; import ReusableCalendar from '$lib/components/reusable/reusable-calendar.svelte';
import { Badge } from "$lib/components/ui/badge/index.js"; import { Badge } from '$lib/components/ui/badge/index.js';
import * as InputGroup from "$lib/components/ui/input-group/index.js"; import * as InputGroup from '$lib/components/ui/input-group/index.js';
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down"; import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
import { Textarea } from "$lib/components/ui/textarea/index.js"; import { Textarea } from '$lib/components/ui/textarea/index.js';
import MoveLeftIcon from "@lucide/svelte/icons/move-left"; import MoveLeftIcon from '@lucide/svelte/icons/move-left';
import MoveRightIcon from "@lucide/svelte/icons/move-right"; import MoveRightIcon from '@lucide/svelte/icons/move-right';
import BrushCleaningIcon from "@lucide/svelte/icons/brush-cleaning"; import BrushCleaningIcon from '@lucide/svelte/icons/brush-cleaning';
import DeleteIcon from "@lucide/svelte/icons/delete"; import DeleteIcon from '@lucide/svelte/icons/delete';
let { let {
formState, formState,
@ -27,7 +27,7 @@
hiddenFields, hiddenFields,
handleTestTypeChange, handleTestTypeChange,
handleResultTypeChange, handleResultTypeChange,
handleRefTypeChange, handleRefTypeChange
} = $props(); } = $props();
const operators = ['+', '-', '*', '/', '^', '(', ')']; const operators = ['+', '-', '*', '/', '^', '(', ')'];
@ -36,12 +36,12 @@
let dropdownOpen = $state({}); let dropdownOpen = $state({});
let expression = $state(''); let expression = $state('');
let cursorPosition = $state(0); let cursorPosition = $state(0);
// $inspect(expression)
function getFilteredOptions(key) { function getFilteredOptions(key) {
const query = searchQuery[key] || ""; const query = searchQuery[key] || '';
if (!query) return formState.selectOptions?.[key] ?? []; if (!query) return formState.selectOptions?.[key] ?? [];
return (formState.selectOptions?.[key] ?? []).filter(opt => return (formState.selectOptions?.[key] ?? []).filter((opt) =>
opt.label.toLowerCase().includes(query.toLowerCase()) opt.label.toLowerCase().includes(query.toLowerCase())
); );
} }
@ -54,7 +54,7 @@
for (const group of formFields) { for (const group of formFields) {
for (const row of group.rows) { for (const row of group.rows) {
for (const col of row.columns) { for (const col of row.columns) {
if (col.type === "group") { if (col.type === 'group') {
for (const child of col.columns) { for (const child of col.columns) {
await handleDefaultValue(child); await handleDefaultValue(child);
} }
@ -81,30 +81,17 @@
formState.validateField?.(key, [], false); formState.validateField?.(key, [], false);
} }
// function getErrorStatus(key) {
// const error = formState.errors[key];
// if (!error) return [];
// const parts = error.split(':');
// if (parts.length < 2) return [];
// const values = parts[1].split(',').map(v => v.trim());
// return values.map(v => ({ value: v, done: false }));
// }
$inspect(formState.form.FormulaInput)
function getErrorStatus(formulaCode = '') { function getErrorStatus(formulaCode = '') {
const selected = formState.form.FormulaInput; const selected = formState.form.FormulaInput;
if (!Array.isArray(selected)) return []; if (!Array.isArray(selected)) return [];
return selected.map(v => ({ return selected.map((v) => ({
value: v, value: v,
done: formulaCode.includes(v) done: new RegExp(`\\b${v}\\b`, 'i').test(formulaCode)
})); }));
} }
function addToExpression(text) { function addToExpression(text) {
console.log(text);
const before = expression.slice(0, cursorPosition); const before = expression.slice(0, cursorPosition);
const after = expression.slice(cursorPosition); const after = expression.slice(cursorPosition);
expression = before + text + after; expression = before + text + after;
@ -113,15 +100,21 @@
function addOperator(op) { function addOperator(op) {
addToExpression(op); addToExpression(op);
formState.form.FormulaCode = expression;
formState.validateField?.('FormulaCode', expression, false);
} }
function addValue(val) { function addValue(val) {
addToExpression(val); addToExpression(val);
formState.form.FormulaCode = expression;
formState.validateField?.('FormulaCode', expression, false);
} }
function handleInput(e) { function handleInput(e) {
expression = e.target.value; expression = e.target.value;
cursorPosition = e.target.selectionStart; cursorPosition = e.target.selectionStart;
formState.form.FormulaCode = expression;
formState.validateField?.('FormulaCode', expression, false);
} }
function handleClick(e) { function handleClick(e) {
@ -156,16 +149,33 @@
const after = expression.slice(cursorPosition); const after = expression.slice(cursorPosition);
expression = before + after; expression = before + after;
cursorPosition -= 1; cursorPosition -= 1;
formState.form.FormulaCode = expression;
formState.validateField?.('FormulaCode', expression, false);
} }
} }
function clearExpression() { function clearExpression() {
expression = ''; expression = '';
cursorPosition = 0; cursorPosition = 0;
formState.form.FormulaCode = expression;
formState.validateField?.('FormulaCode', expression, false);
} }
</script> </script>
{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey, valueKey, labelKey, txtKey })} {#snippet Fieldset({
key,
label,
required,
type,
optionsEndpoint,
options,
validateOn,
dependsOn,
endpointParamKey,
valueKey,
labelKey,
txtKey
})}
<div class="flex w-full flex-col gap-1.5"> <div class="flex w-full flex-col gap-1.5">
<div class="flex justify-between items-center w-full"> <div class="flex justify-between items-center w-full">
<Label>{label}</Label> <Label>{label}</Label>
@ -175,83 +185,87 @@
</div> </div>
<div class="relative flex flex-col items-center w-full"> <div class="relative flex flex-col items-center w-full">
{#if type === "text"} {#if type === 'text'}
<Input <Input
type="text" type="text"
bind:value={formState.form[key]} bind:value={formState.form[key]}
oninput={() => { oninput={() => {
// console.log(`key: ${key}, form: ${formState.form[key]}`); // console.log(`key: ${key}, form: ${formState.form[key]}`);
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField(key, formState.form[key], false); formState.validateField(key, formState.form[key], false);
} }
}} }}
onblur={() => { onblur={() => {
if (validateOn?.includes("blur")) { if (validateOn?.includes('blur')) {
validateFieldAsync(key, mode, originalData?.[key]); validateFieldAsync(key, mode, originalData?.[key]);
} }
}} }}
readonly={key === "NumRefType" || key === "TxtRefType" || key === "Level"} readonly={key === 'NumRefType' || key === 'TxtRefType' || key === 'Level'}
/> />
{:else if type === "email"} {:else if type === 'email'}
<Input <Input
type="email" type="email"
bind:value={formState.form[key]} bind:value={formState.form[key]}
oninput={() => { oninput={() => {
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField(key, formState.form[key], false); formState.validateField(key, formState.form[key], false);
} }
}} }}
onblur={() => { onblur={() => {
if (validateOn?.includes("blur")) { if (validateOn?.includes('blur')) {
formState.validateField(key, formState.form[key], false); formState.validateField(key, formState.form[key], false);
} }
}} }}
/> />
{:else if type === "number"} {:else if type === 'number'}
<Input <Input
type="number" type="number"
bind:value={formState.form[key]} bind:value={formState.form[key]}
oninput={() => { oninput={() => {
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField(key, formState.form[key], false); formState.validateField(key, formState.form[key], false);
} }
}} }}
onblur={() => { onblur={() => {
if (validateOn?.includes("blur")) { if (validateOn?.includes('blur')) {
formState.validateField(key, formState.form[key], false); formState.validateField(key, formState.form[key], false);
} }
}} }}
onkeydown={(e) => ['e', 'E', '+', '-'].includes(e.key) && e.preventDefault()} onkeydown={(e) => ['e', 'E', '+', '-'].includes(e.key) && e.preventDefault()}
/> />
{:else if type === "textarea"} {:else if type === 'textarea'}
<Textarea <Textarea
class="flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" class="flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
oninput={() => { oninput={() => {
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField(key, formState.form[key], false); formState.validateField(key, formState.form[key], false);
} }
}} }}
onblur={() => { onblur={() => {
if (validateOn?.includes("blur")) { if (validateOn?.includes('blur')) {
formState.validateField(key, formState.form[key], false); formState.validateField(key, formState.form[key], false);
} }
}} }}
bind:value={formState.form[key]} bind:value={formState.form[key]}
/> />
{:else if type === "select"} {:else if type === 'select'}
{@const selectedLabel = formState.selectOptions?.[key]?.find(opt => opt.value === formState.form[key])?.label || "Choose"} {@const selectedLabel =
formState.selectOptions?.[key]?.find((opt) => opt.value === formState.form[key])?.label ||
'Choose'}
{@const filteredOptions = getFilteredOptions(key)} {@const filteredOptions = getFilteredOptions(key)}
<Select.Root type="single" bind:value={formState.form[key]} <Select.Root
type="single"
bind:value={formState.form[key]}
onValueChange={(val) => { onValueChange={(val) => {
formState.form[key] = val; formState.form[key] = val;
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField?.(key, formState.form[key], false); formState.validateField?.(key, formState.form[key], false);
} }
if (key === "TestType") { if (key === 'TestType') {
handleTestTypeChange(val); handleTestTypeChange(val);
} }
if (key === "Province") { if (key === 'Province') {
formState.form.City = ""; formState.form.City = '';
if (formState.selectOptions) { if (formState.selectOptions) {
formState.selectOptions.City = []; formState.selectOptions.City = [];
} }
@ -259,35 +273,35 @@
formState.lastFetched.City = null; formState.lastFetched.City = null;
} }
} }
if (key === "ResultType") { if (key === 'ResultType') {
handleResultTypeChange(val); handleResultTypeChange(val);
} }
if (key === "RefType") { if (key === 'RefType') {
handleRefTypeChange(val); handleRefTypeChange(val);
} }
if (key === "HostType") { if (key === 'HostType') {
formState.form.HostID = ""; formState.form.HostID = '';
formState.form.HostTestCode = ""; formState.form.HostTestCode = '';
formState.form.HostTestName = ""; formState.form.HostTestName = '';
formState.selectOptions.HostTestCode = []; formState.selectOptions.HostTestCode = [];
formState.selectOptions.HostTestName = []; formState.selectOptions.HostTestName = [];
} }
if (key === "HostID") { if (key === 'HostID') {
formState.form.HostTestCode = ""; formState.form.HostTestCode = '';
formState.form.HostTestName = ""; formState.form.HostTestName = '';
formState.selectOptions.HostTestCode = []; formState.selectOptions.HostTestCode = [];
formState.selectOptions.HostTestName = []; formState.selectOptions.HostTestName = [];
} }
if (key === "ClientType") { if (key === 'ClientType') {
formState.form.ClientID = ""; formState.form.ClientID = '';
formState.form.ClientTestCode = ""; formState.form.ClientTestCode = '';
formState.form.ClientTestName = ""; formState.form.ClientTestName = '';
formState.selectOptions.ClientTestCode = []; formState.selectOptions.ClientTestCode = [];
formState.selectOptions.ClientTestName = []; formState.selectOptions.ClientTestName = [];
} }
if (key === "ClientID") { if (key === 'ClientID') {
formState.form.ClientTestCode = ""; formState.form.ClientTestCode = '';
formState.form.ClientTestName = ""; formState.form.ClientTestName = '';
formState.selectOptions.ClientTestCode = []; formState.selectOptions.ClientTestCode = [];
formState.selectOptions.ClientTestName = []; formState.selectOptions.ClientTestName = [];
} }
@ -320,9 +334,10 @@
<Select.Item value="">- None -</Select.Item> <Select.Item value="">- None -</Select.Item>
{/if} {/if}
{#each filteredOptions as option} {#each filteredOptions as option}
<Select.Item value={option.value} <Select.Item
disabled={key === "ResultType" && disabledResultTypes.includes(option.value) || value={option.value}
key === "RefType" && disabledReferenceTypes.includes(option.value)} disabled={(key === 'ResultType' && disabledResultTypes.includes(option.value)) ||
(key === 'RefType' && disabledReferenceTypes.includes(option.value))}
> >
{option.label} {option.label}
</Select.Item> </Select.Item>
@ -330,16 +345,16 @@
{/if} {/if}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
{:else if type === "selectmultiple"} {:else if type === 'selectmultiple'}
{@const filteredOptions = getFilteredOptions(key)} {@const filteredOptions = getFilteredOptions(key)}
<Select.Root <Select.Root
type="multiple" type="multiple"
bind:value={formState.form[key]} bind:value={formState.form[key]}
onValueChange={(val) => { onValueChange={(val) => {
formState.form[key] = val; formState.form[key] = val;
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField?.(key, val, false); formState.validateField?.(key, val, false);
formState.validateField?.("FormulaCode", val, false); formState.validateField?.('FormulaCode', expression, false);
} }
}} }}
onOpenChange={(open) => { onOpenChange={(open) => {
@ -354,8 +369,8 @@
<Select.Trigger class="w-full"> <Select.Trigger class="w-full">
{formState.form[key]?.length {formState.form[key]?.length
? (formState.selectOptions?.[key] ?? []) ? (formState.selectOptions?.[key] ?? [])
.filter(o => formState.form[key].includes(o.value)) .filter((o) => formState.form[key].includes(o.value))
.map(o => o.label) .map((o) => o.label)
.join(', ') .join(', ')
: 'Choose'} : 'Choose'}
</Select.Trigger> </Select.Trigger>
@ -370,9 +385,7 @@
</div> </div>
<Select.Group> <Select.Group>
{#if formState.loadingOptions?.[key]} {#if formState.loadingOptions?.[key]}
<div class="p-2 text-sm text-muted-foreground"> <div class="p-2 text-sm text-muted-foreground">Loading...</div>
Loading...
</div>
{:else} {:else}
{#if formState.form[key].length > 0} {#if formState.form[key].length > 0}
<Select.Separator /> <Select.Separator />
@ -392,27 +405,27 @@
</Select.Group> </Select.Group>
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
{:else if type === "date"} {:else if type === 'date'}
<ReusableCalendar <ReusableCalendar
bind:value={formState.form[key]} bind:value={formState.form[key]}
parentFunction={(dateStr) => { parentFunction={(dateStr) => {
formState.form[key] = dateStr; formState.form[key] = dateStr;
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField(key, dateStr, false); formState.validateField(key, dateStr, false);
} }
}} }}
/> />
{:else if type === "signvalue"} {:else if type === 'signvalue'}
<InputGroup.Root> <InputGroup.Root>
<InputGroup.Input <InputGroup.Input
placeholder="Type here" placeholder="Type here"
bind:value={formState.form[txtKey]} bind:value={formState.form[txtKey]}
oninput={() => { oninput={() => {
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
formState.validateField("Low", formState.form[txtKey]); formState.validateField('Low', formState.form[txtKey]);
formState.validateField("LowSign"); formState.validateField('LowSign');
formState.validateField("High", formState.form[txtKey]); formState.validateField('High', formState.form[txtKey]);
formState.validateField("HighSign"); formState.validateField('HighSign');
} }
}} }}
/> />
@ -430,27 +443,24 @@
> >
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
{#snippet child({ props })} {#snippet child({ props })}
<InputGroup.Button <InputGroup.Button {...props} variant="ghost" disabled={disabledSign}>
{...props} {formState.selectOptions?.[key]?.find(
variant="ghost" disabled={disabledSign} (opt) => opt.value === formState.form[key]
> )?.label || 'Choose'}
{formState.selectOptions?.[key]?.find(opt => opt.value === formState.form[key])?.label || 'Choose'}
<ChevronDownIcon /> <ChevronDownIcon />
</InputGroup.Button> </InputGroup.Button>
{/snippet} {/snippet}
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content align="start"> <DropdownMenu.Content align="start">
{#if formState.loadingOptions?.[key]} {#if formState.loadingOptions?.[key]}
<DropdownMenu.Item disabled> <DropdownMenu.Item disabled>Loading...</DropdownMenu.Item>
Loading...
</DropdownMenu.Item>
{:else} {:else}
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => { onSelect={() => {
formState.form[key] = ""; formState.form[key] = '';
dropdownOpen[key] = false; dropdownOpen[key] = false;
formState.validateField("LowSign"); formState.validateField('LowSign');
formState.validateField("HighSign"); formState.validateField('HighSign');
}} }}
> >
- None - - None -
@ -463,8 +473,8 @@
}} }}
onSelect={() => { onSelect={() => {
formState.form[key] = option.value; formState.form[key] = option.value;
formState.validateField("LowSign"); formState.validateField('LowSign');
formState.validateField("HighSign"); formState.validateField('HighSign');
}} }}
> >
{option.label} {option.label}
@ -475,15 +485,17 @@
</DropdownMenu.Root> </DropdownMenu.Root>
</InputGroup.Addon> </InputGroup.Addon>
</InputGroup.Root> </InputGroup.Root>
{:else if type === "agejoin"} {:else if type === 'agejoin'}
<div class="flex items-center gap-2 w-full"> <div class="flex items-center gap-2 w-full">
<InputGroup.Root> <InputGroup.Root>
<InputGroup.Input type="number" bind:value={joinFields[key].YY} <InputGroup.Input
type="number"
bind:value={joinFields[key].YY}
oninput={() => { oninput={() => {
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
// formState.validateField(key, formState.form[key], false); // formState.validateField(key, formState.form[key], false);
formState.validateField("AgeStart"); formState.validateField('AgeStart');
formState.validateField("AgeEnd"); formState.validateField('AgeEnd');
} }
}} }}
/> />
@ -492,12 +504,14 @@
</InputGroup.Addon> </InputGroup.Addon>
</InputGroup.Root> </InputGroup.Root>
<InputGroup.Root> <InputGroup.Root>
<InputGroup.Input type="number" bind:value={joinFields[key].MM} <InputGroup.Input
type="number"
bind:value={joinFields[key].MM}
oninput={() => { oninput={() => {
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
// formState.validateField(key, formState.form[key], false); // formState.validateField(key, formState.form[key], false);
formState.validateField("AgeStart"); formState.validateField('AgeStart');
formState.validateField("AgeEnd"); formState.validateField('AgeEnd');
} }
}} }}
/> />
@ -506,12 +520,14 @@
</InputGroup.Addon> </InputGroup.Addon>
</InputGroup.Root> </InputGroup.Root>
<InputGroup.Root> <InputGroup.Root>
<InputGroup.Input type="number" bind:value={joinFields[key].DD} <InputGroup.Input
type="number"
bind:value={joinFields[key].DD}
oninput={() => { oninput={() => {
if (validateOn?.includes("input")) { if (validateOn?.includes('input')) {
// formState.validateField(key, formState.form[key], false); // formState.validateField(key, formState.form[key], false);
formState.validateField("AgeStart"); formState.validateField('AgeStart');
formState.validateField("AgeEnd"); formState.validateField('AgeEnd');
} }
}} }}
/> />
@ -520,19 +536,14 @@
</InputGroup.Addon> </InputGroup.Addon>
</InputGroup.Root> </InputGroup.Root>
</div> </div>
{:else if type === "formulabuilder"} {:else if type === 'formulabuilder'}
<div class="flex flex-col gap-8 w-full"> <div class="flex flex-col gap-8 w-full">
<div class="flex gap-1 w-full"> <div class="flex gap-1 w-full">
<Button <Button type="button" variant="outline" size="icon" onclick={moveCursorLeft}>
type="button"
variant="outline"
size="icon"
onclick={moveCursorLeft}
>
<MoveLeftIcon class="w-4 h-4" /> <MoveLeftIcon class="w-4 h-4" />
</Button> </Button>
<div class="relative flex-1"> <div class="relative flex-1">
<div <button
class="flex flex-1 h-9 w-full min-w-0 items-center rounded-md border bg-background px-3 py-2 font-mono text-sm whitespace-nowrap" class="flex flex-1 h-9 w-full min-w-0 items-center rounded-md border bg-background px-3 py-2 font-mono text-sm whitespace-nowrap"
role="textbox" role="textbox"
tabindex="0" tabindex="0"
@ -547,31 +558,16 @@
{#if cursorPosition === expression.length} {#if cursorPosition === expression.length}
<span class="animate-cursor inline-block h-6 w-0.5 bg-primary"></span> <span class="animate-cursor inline-block h-6 w-0.5 bg-primary"></span>
{/if} {/if}
</button>
</div> </div>
</div> <Button type="button" variant="outline" size="icon" onclick={moveCursorRight}>
<Button
type="button"
variant="outline"
size="icon"
onclick={moveCursorRight}
>
<MoveRightIcon class="w-4 h-4" /> <MoveRightIcon class="w-4 h-4" />
</Button> </Button>
<Button <Button type="button" variant="outline" size="icon" onclick={deleteChar}>
type="button"
variant="outline"
size="icon"
onclick={deleteChar}
>
<DeleteIcon class="w-4 h-4" /> <DeleteIcon class="w-4 h-4" />
</Button> </Button>
<Button <Button type="button" variant="outline" size="icon" onclick={clearExpression}>
type="button"
variant="outline"
size="icon"
onclick={clearExpression}
>
<BrushCleaningIcon class="w-4 h-4" /> <BrushCleaningIcon class="w-4 h-4" />
</Button> </Button>
</div> </div>
@ -584,14 +580,13 @@
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="icon" class="h-auto w-auto p-2"
onclick={() => addValue(item)} onclick={() => addValue(item)}
> >
{item} {item}
</Button> </Button>
{/each} {/each}
</div> </div>
</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-sm font-medium">Operators</span> <span class="text-sm font-medium">Operators</span>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@ -608,6 +603,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{/if} {/if}
</div> </div>
{:else} {:else}
@ -617,11 +613,7 @@
placeholder="Custom field type: {type}" placeholder="Custom field type: {type}"
/> />
{/if} {/if}
<div <div class={`absolute min-h-[1rem] w-full ${key === 'FormulaCode' ? 'top-10' : 'top-10'}`}>
class={`absolute min-h-[1rem] w-full ${
key === 'FormulaCode' ? 'top-10' : 'top-8'
}`}
>
{#if key !== 'FormulaCode' && (formState.errors[key] || formState.errors[txtKey])} {#if key !== 'FormulaCode' && (formState.errors[key] || formState.errors[txtKey])}
{@const errorMessage = formState.errors[key] ?? formState.errors[txtKey]} {@const errorMessage = formState.errors[key] ?? formState.errors[txtKey]}
@ -633,7 +625,7 @@
{@const inputStatus = getErrorStatus(expression)} {@const inputStatus = getErrorStatus(expression)}
<div class="flex items-center gap-2 text-sm text-destructive"> <div class="flex items-center gap-2 text-sm text-destructive">
<span>Formula parameters:</span> <span>Must included :</span>
<div class="flex gap-1 flex-wrap"> <div class="flex gap-1 flex-wrap">
{#each inputStatus as item (item.value)} {#each inputStatus as item (item.value)}
@ -659,24 +651,31 @@
{/if} {/if}
{#each group.rows as row} {#each group.rows as row}
{@const visibleColumns = row.columns.filter(col => !hiddenFields?.includes(col.key))} {@const visibleColumns = row.columns.filter((col) => !hiddenFields?.includes(col.key))}
{#if visibleColumns.length > 0} {#if visibleColumns.length > 0}
<div <div
class="grid grid-cols-1 space-y-2 gap-6 md:gap-4" class="grid grid-cols-1 space-y-2 gap-6 md:gap-4"
class:md:grid-cols-1={visibleColumns.length === 1 && visibleColumns[0].fullWidth !== false} class:md:grid-cols-1={visibleColumns.length === 1 &&
class:md:grid-cols-2={visibleColumns.length === 2 || (visibleColumns.length === 1 && visibleColumns[0].fullWidth === false)} visibleColumns[0].fullWidth !== false}
class:md:grid-cols-2={visibleColumns.length === 2 ||
(visibleColumns.length === 1 && visibleColumns[0].fullWidth === false)}
class:md:grid-cols-3={visibleColumns.length === 3} class:md:grid-cols-3={visibleColumns.length === 3}
> >
{#each visibleColumns as col} {#each visibleColumns as col}
{#if col.type === "group"} {#if col.type === 'group'}
{@const visibleChildColumns = col.columns.filter(child => !hiddenFields?.includes(child.key))} {@const visibleChildColumns = col.columns.filter(
(child) => !hiddenFields?.includes(child.key)
)}
{#if visibleChildColumns.length > 0} {#if visibleChildColumns.length > 0}
<div <div
class="grid grid-cols-1 gap-6 md:gap-2" class="grid grid-cols-1 gap-6 md:gap-2"
class:md:grid-cols-1={visibleChildColumns.length === 1 && visibleChildColumns[0].fullWidth !== false} class:md:grid-cols-1={visibleChildColumns.length === 1 &&
class:md:grid-cols-2={visibleChildColumns.length === 2 || (visibleChildColumns.length === 1 && visibleChildColumns[0].fullWidth === false)} visibleChildColumns[0].fullWidth !== false}
class:md:grid-cols-2={visibleChildColumns.length === 2 ||
(visibleChildColumns.length === 1 &&
visibleChildColumns[0].fullWidth === false)}
class:md:grid-cols-3={visibleChildColumns.length === 3} class:md:grid-cols-3={visibleChildColumns.length === 3}
> >
{#each visibleChildColumns as child} {#each visibleChildColumns as child}