diff --git a/apps/client/src/features/base/components/views/view-filter-config.tsx b/apps/client/src/features/base/components/views/view-filter-config.tsx index 412def29..aae01f74 100644 --- a/apps/client/src/features/base/components/views/view-filter-config.tsx +++ b/apps/client/src/features/base/components/views/view-filter-config.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Popover, Stack, @@ -8,6 +8,7 @@ import { ActionIcon, Text, UnstyledButton, + Button, } from "@mantine/core"; import { IconPlus, IconTrash } from "@tabler/icons-react"; import { @@ -173,18 +174,69 @@ export function ViewFilterConfigPopover({ label: p.name, })); - const handleAdd = useCallback(() => { + const [draft, setDraft] = useState(null); + + useEffect(() => { + if (!opened) setDraft(null); + }, [opened]); + + const handleStartDraft = useCallback(() => { const firstProperty = properties[0]; if (!firstProperty) return; const validOperators = getOperatorsForType(firstProperty.type); const defaultOperator = validOperators.includes("contains") ? ("contains" as FilterOperator) : validOperators[0]; - onChange([ - ...conditions, - { propertyId: firstProperty.id, op: defaultOperator }, - ]); - }, [conditions, properties, onChange]); + setDraft({ propertyId: firstProperty.id, op: defaultOperator }); + }, [properties]); + + const handleSaveDraft = useCallback(() => { + if (!draft) return; + onChange([...conditions, draft]); + setDraft(null); + }, [draft, conditions, onChange]); + + const handleCancelDraft = useCallback(() => { + setDraft(null); + }, []); + + const handleDraftPropertyChange = useCallback( + (propertyId: string | null) => { + if (!propertyId || !draft) return; + const newProperty = properties.find((p) => p.id === propertyId); + if (!newProperty) { + setDraft({ ...draft, propertyId }); + return; + } + const validOperators = getOperatorsForType(newProperty.type); + const currentOperatorValid = validOperators.includes(draft.op); + setDraft({ + ...draft, + propertyId, + op: currentOperatorValid ? draft.op : validOperators[0], + value: currentOperatorValid ? draft.value : undefined, + }); + }, + [draft, properties], + ); + + const handleDraftOperatorChange = useCallback( + (operator: string | null) => { + if (!operator || !draft) return; + const op = operator as FilterOperator; + const needsValue = !NO_VALUE_OPERATORS.includes(op); + setDraft({ ...draft, op, value: needsValue ? draft.value : undefined }); + }, + [draft], + ); + + const handleDraftValueChange = useCallback( + (value: string) => { + if (!draft) return; + setDraft({ ...draft, value: value || undefined }); + }, + [draft], + ); const handleRemove = useCallback( (index: number) => { @@ -265,7 +317,7 @@ export function ViewFilterConfigPopover({ {t("Filter by")} - {conditions.length === 0 && ( + {conditions.length === 0 && !draft && ( {t("No filters applied")} @@ -322,20 +374,70 @@ export function ViewFilterConfigPopover({ ); })} - - - {t("Add filter")} - + {draft && (() => { + const needsValue = !NO_VALUE_OPERATORS.includes(draft.op); + const property = properties.find((p) => p.id === draft.propertyId); + const validOperators = property + ? getOperatorsForType(property.type) + : OPERATORS.map((op) => op.value); + const operatorOptions = OPERATORS.filter((op) => + validOperators.includes(op.value), + ).map((op) => ({ value: op.value, label: t(op.labelKey) })); + + return ( + + + + {needsValue && ( + + )} + + + + + + + ); + })()} + + {!draft && ( + + + {t("Add filter")} + + )}