Compare commits

...
Author SHA1 Message Date
Salihu 75c2822fd7 fix classification bug 2026-08-21 01:07:40 +01:00
Salihu 89b7bb8778 rework filtering behaviour 2026-08-21 00:51:50 +01:00
@@ -9,6 +9,7 @@ import {
Text, Text,
UnstyledButton, UnstyledButton,
Button, Button,
MultiSelect,
} from "@mantine/core"; } from "@mantine/core";
import { IconPlus, IconTrash } from "@tabler/icons-react"; import { IconPlus, IconTrash } from "@tabler/icons-react";
import { import {
@@ -52,6 +53,9 @@ const NO_VALUE_OPERATORS: FilterOperator[] = ["isEmpty", "isNotEmpty"];
// stored value so a stale shape isn't sent to the engine. // stored value so a stale shape isn't sent to the engine.
function valueClass(op: FilterOperator, inputKind: string): string { function valueClass(op: FilterOperator, inputKind: string): string {
if (NO_VALUE_OPERATORS.includes(op)) return "none"; if (NO_VALUE_OPERATORS.includes(op)) return "none";
if (inputKind === "choices") {
return op === "any" || op === "none" ? "choicesMulti" : "choicesSingle";
}
if (inputKind === "person") { if (inputKind === "person") {
return op === "any" || op === "none" ? "personMulti" : "personSingle"; return op === "any" || op === "none" ? "personMulti" : "personSingle";
} }
@@ -70,6 +74,10 @@ function getOperatorsForType(type: string): FilterOperator[] {
DEFAULT_FILTER_OPERATORS) as FilterOperator[]; DEFAULT_FILTER_OPERATORS) as FilterOperator[];
} }
function isMultiChoice(op: FilterCondition["op"]): boolean {
return op === "any" || op === "none";
}
function FilterValueInput({ function FilterValueInput({
condition, condition,
property, property,
@@ -121,6 +129,32 @@ function FilterValueInput({
const typeOptions = property.typeOptions as SelectTypeOptions | undefined; const typeOptions = property.typeOptions as SelectTypeOptions | undefined;
const choices = typeOptions?.choices ?? []; const choices = typeOptions?.choices ?? [];
const choiceOptions = choices.map((c) => ({ value: c.id, label: c.name })); const choiceOptions = choices.map((c) => ({ value: c.id, label: c.name }));
if (isMultiChoice(condition.op)) {
const { value } = condition;
const selected = (
Array.isArray(value) ? value : value ? [value] : []
).filter((id) => choices.some((c) => c.id === id));
return (
<MultiSelect
size="xs"
data={choiceOptions}
comboboxProps={{ withinPortal: false }}
value={selected}
onChange={(values) => onChange(values)}
w={160}
styles={{
pillsList: {
maxHeight: 70,
overflowY: "auto",
},
}}
maxDropdownHeight={220}
/>
);
}
return ( return (
<Select <Select
size="xs" size="xs"
@@ -199,11 +233,18 @@ export function ViewFilterConfigPopover({
label: p.name, label: p.name,
})); }));
const [unSaved, setUnSaved] = useState(false)
const [draft, setDraft] = useState<FilterCondition | null>(null); const [draft, setDraft] = useState<FilterCondition | null>(null);
const [draftConditions, setDraftConditions] =
useState<FilterCondition[]>(conditions);
useEffect(() => { useEffect(() => {
if (!opened) setDraft(null); if (opened) {
}, [opened]); setDraftConditions(conditions);
setDraft(null);
setUnSaved(false)
}
}, [opened, conditions]);
const handleStartDraft = useCallback(() => { const handleStartDraft = useCallback(() => {
const firstProperty = properties[0]; const firstProperty = properties[0];
@@ -216,14 +257,21 @@ export function ViewFilterConfigPopover({
}, [properties]); }, [properties]);
const handleSaveDraft = useCallback(() => { const handleSaveDraft = useCallback(() => {
if (!draft) return; const nextConditions = draft
onChange([...conditions, draft]); ? [...draftConditions, draft]
: draftConditions;
onChange(nextConditions);
setDraft(null); setDraft(null);
}, [draft, conditions, onChange]); setUnSaved(false)
}, [draft, draftConditions, onChange]);
const handleCancelDraft = useCallback(() => { const handleCancelDraft = useCallback(() => {
setDraftConditions(conditions)
setDraft(null); setDraft(null);
}, []); setUnSaved(false)
}, [conditions]);
const handleDraftPropertyChange = useCallback( const handleDraftPropertyChange = useCallback(
(propertyId: string | null) => { (propertyId: string | null) => {
@@ -272,17 +320,19 @@ export function ViewFilterConfigPopover({
const handleRemove = useCallback( const handleRemove = useCallback(
(index: number) => { (index: number) => {
onChange(conditions.filter((_, i) => i !== index)); setUnSaved(true);
setDraftConditions((current) => current.filter((_, i) => i !== index));
}, },
[conditions, onChange], [],
); );
const handlePropertyChange = useCallback( const handlePropertyChange = useCallback(
(index: number, propertyId: string | null) => { (index: number, propertyId: string | null) => {
if (!propertyId) return; if (!propertyId) return;
const newProperty = properties.find((p) => p.id === propertyId); const newProperty = properties.find((p) => p.id === propertyId);
onChange( setUnSaved(true)
conditions.map((f, i) => { setDraftConditions((current) =>
current.map((f, i) => {
if (i !== index) return f; if (i !== index) return f;
if (newProperty) { if (newProperty) {
const validOperators = getOperatorsForType(newProperty.type); const validOperators = getOperatorsForType(newProperty.type);
@@ -302,15 +352,16 @@ export function ViewFilterConfigPopover({
}), }),
); );
}, },
[conditions, properties, onChange], [properties],
); );
const handleOperatorChange = useCallback( const handleOperatorChange = useCallback(
(index: number, operator: string | null) => { (index: number, operator: string | null) => {
if (!operator) return; if (!operator) return;
const op = operator as FilterOperator; const op = operator as FilterOperator;
onChange( setUnSaved(true)
conditions.map((f, i) => { setDraftConditions((current) =>
current.map((f, i) => {
if (i !== index) return f; if (i !== index) return f;
const kind = inputKindForProperty( const kind = inputKindForProperty(
properties.find((p) => p.id === f.propertyId), properties.find((p) => p.id === f.propertyId),
@@ -320,16 +371,17 @@ export function ViewFilterConfigPopover({
}), }),
); );
}, },
[conditions, properties, onChange], [properties],
); );
const handleValueChange = useCallback( const handleValueChange = useCallback(
(index: number, value: unknown) => { (index: number, value: unknown) => {
onChange( setUnSaved(true)
conditions.map((f, i) => (i === index ? { ...f, value } : f)), setDraftConditions((current) =>
current.map((f, i) => (i === index ? { ...f, value } : f)),
); );
}, },
[conditions, onChange], [],
); );
return ( return (
@@ -362,13 +414,13 @@ export function ViewFilterConfigPopover({
{t("Filter by")} {t("Filter by")}
</Text> </Text>
{conditions.length === 0 && !draft && ( {draftConditions.length === 0 && !draft && (
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{t("No filters applied")} {t("No filters applied")}
</Text> </Text>
)} )}
{conditions.map((condition, index) => { {draftConditions.map((condition, index) => {
const needsValue = !NO_VALUE_OPERATORS.includes(condition.op); const needsValue = !NO_VALUE_OPERATORS.includes(condition.op);
const property = properties.find( const property = properties.find(
(p) => p.id === condition.propertyId, (p) => p.id === condition.propertyId,
@@ -471,14 +523,6 @@ export function ViewFilterConfigPopover({
/> />
)} )}
</Group> </Group>
<Group justify="flex-end" gap="xs">
<Button variant="default" size="xs" onClick={handleCancelDraft}>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSaveDraft}>
{t("Save")}
</Button>
</Group>
</Stack> </Stack>
); );
})()} })()}
@@ -492,6 +536,20 @@ export function ViewFilterConfigPopover({
{t("Add filter")} {t("Add filter")}
</UnstyledButton> </UnstyledButton>
)} )}
<Group justify="flex-end" gap="xs">
<Button
variant="default"
size="xs"
onClick={handleCancelDraft}
disabled={!draft && !unSaved}
>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSaveDraft} disabled={!draft && !unSaved}>
{t("Save")}
</Button>
</Group>
</Stack> </Stack>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>