Skip to content

Commit 5f5f5af

Browse files
authored
Improvements and fixes on object table filters (#5802)
- Fix isnull condition - Extracted code logic for filters to graphql query - enum list is visible instead of text in table filters - align kind column
1 parent a36bfd7 commit 5f5f5af

File tree

8 files changed

+206
-31
lines changed

8 files changed

+206
-31
lines changed

frontend/app/src/entities/nodes/object/domain/get-objects.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { getRelationshipsVisibleInListView } from "@/entities/nodes/object/utils
33
import { NodeObject } from "@/entities/nodes/types";
44
import { IModelSchema } from "@/entities/schema/stores/schema.atom";
55
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
6-
import { addAttributesToRequest, addRelationshipsToRequest } from "@/shared/api/graphql/utils";
6+
import {
7+
addAttributesToRequest,
8+
addFiltersToRequest,
9+
addRelationshipsToRequest,
10+
} from "@/shared/api/graphql/utils";
711
import { ContextParams, PaginationParams } from "@/shared/api/types";
812
import { Filter } from "@/shared/hooks/useFilters";
913
import { gql } from "@apollo/client";
@@ -45,29 +49,7 @@ export const getObjects: GetObjects = async ({
4549
__args: {
4650
limit,
4751
offset,
48-
...(filters
49-
? filters.reduce(
50-
(acc, filter) => {
51-
if (filter.name === "kind__value") return acc;
52-
53-
const [fieldName, fieldKey] = filter.name.split("__");
54-
55-
if (!fieldName || !fieldKey) return acc;
56-
57-
if (fieldKey === "value" || fieldKey === "values" || fieldKey === "isnull") {
58-
acc[filter.name] = filter.value;
59-
return acc;
60-
}
61-
62-
if (fieldKey === "ids") {
63-
acc[filter.name] = filter.value.map(({ id }: { id: string }) => id);
64-
}
65-
66-
return acc;
67-
},
68-
{ partial_match: true } as Record<string, string | number | boolean>
69-
)
70-
: {}),
52+
...(filters ? addFiltersToRequest(filters) : {}),
7153
},
7254
edges: {
7355
node: {

frontend/app/src/entities/nodes/object/ui/filters/attribute-filter-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function AttributeFilterForm({ attributeSchema, onSuccess }: AttributeFil
5050
...filters.filter((f) => f.name !== currentFilter?.name),
5151
{
5252
name: `${attributeSchema.name}__isnull`,
53-
value: false,
53+
value: true,
5454
},
5555
]);
5656
}

frontend/app/src/entities/nodes/object/ui/filters/dynamic-filter-input.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { JsonEditor } from "@/shared/components/editor/json/json-editor";
77
import { ColorPicker } from "@/shared/components/inputs/color-picker";
88
import { DatePicker } from "@/shared/components/inputs/date-picker";
99
import { Dropdown, DropdownOption } from "@/shared/components/inputs/dropdown";
10+
import { Enum } from "@/shared/components/inputs/enum";
1011
import { List } from "@/shared/components/list";
1112
import { Badge } from "@/shared/components/ui/badge";
1213
import { Combobox, ComboboxContent } from "@/shared/components/ui/combobox";
@@ -77,8 +78,22 @@ export function DynamicFilterInput({ fieldSchema, value, onChange }: DynamicFilt
7778

7879
const fieldKind = fieldSchema.kind as AttributeKind;
7980
switch (fieldKind) {
81+
case ATTRIBUTE_KIND.TEXT: {
82+
if (fieldSchema.enum) {
83+
return (
84+
<Enum
85+
items={fieldSchema.enum as string[]}
86+
value={value}
87+
onChange={onChange}
88+
defaultOpen={true}
89+
fitTriggerWidth={false}
90+
className="min-w-[132px]"
91+
/>
92+
);
93+
}
94+
return <Input autoFocus value={value} onChange={onChange} />;
95+
}
8096
case ATTRIBUTE_KIND.ID:
81-
case ATTRIBUTE_KIND.TEXT:
8297
case ATTRIBUTE_KIND.TEXTAREA:
8398
case ATTRIBUTE_KIND.EMAIL:
8499
case ATTRIBUTE_KIND.FILE:

frontend/app/src/entities/nodes/object/ui/object-table/cells/generics/kind-header-cell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function KindHeaderCell({ schema }: { schema: iGenericSchema }) {
3737
/>
3838
</PopoverTrigger>
3939

40-
<PopoverContent className="min-w-[19rem] relative rounded-tl-none">
40+
<PopoverContent className="min-w-[19rem] relative rounded-tl-none" align="start">
4141
<div className="absolute font-semibold -top-[1.8rem] bg-white border px-2 py-1 rounded-t-md border-b-0 -left-px">
4242
Filter by kind
4343
</div>

frontend/app/src/shared/api/graphql/utils.test.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { AttributeSchema, RelationshipSchema } from "@/entities/schema/types";
2+
import { Filter } from "@/shared/hooks/useFilters";
23
import { describe, expect, it } from "vitest";
34
import { generateAttributeSchema, generateRelationshipSchema } from "../../../../tests/fake/schema";
4-
import { addAttributesToRequest, addRelationshipsToRequest } from "./utils";
5+
import { addAttributesToRequest, addFiltersToRequest, addRelationshipsToRequest } from "./utils";
56

67
describe("addAttributesToRequest", () => {
78
it("should return base fragment for simple attribute", () => {
@@ -249,3 +250,104 @@ describe("addRelationshipsToRequest", () => {
249250
});
250251
});
251252
});
253+
254+
describe("addFiltersToRequest", () => {
255+
it("should not include kind__value filter in the result", () => {
256+
// GIVEN
257+
const filters: Filter[] = [{ name: "kind__value", value: "test" }];
258+
259+
// WHEN
260+
const result = addFiltersToRequest(filters);
261+
262+
// THEN
263+
expect(result).toEqual({});
264+
});
265+
266+
it("should add partial_match flag and value for text-based value filters", () => {
267+
// GIVEN
268+
const filters: Filter[] = [{ name: "name__value", value: "test" }];
269+
270+
// WHEN
271+
const result = addFiltersToRequest(filters);
272+
273+
// THEN
274+
expect(result).toEqual({
275+
partial_match: true,
276+
name__value: "test",
277+
});
278+
});
279+
280+
it("should add partial_match flag and array value for text-based values filters", () => {
281+
// GIVEN
282+
const filters: Filter[] = [{ name: "tags__values", value: ["tag1", "tag2"] }];
283+
284+
// WHEN
285+
const result = addFiltersToRequest(filters);
286+
287+
// THEN
288+
expect(result).toEqual({
289+
partial_match: true,
290+
tags__values: ["tag1", "tag2"],
291+
});
292+
});
293+
294+
it("should include isnull filter value without partial_match flag", () => {
295+
// GIVEN
296+
const filters: Filter[] = [{ name: "field__isnull", value: true }];
297+
298+
// WHEN
299+
const result = addFiltersToRequest(filters);
300+
301+
// THEN
302+
expect(result).toEqual({
303+
field__isnull: true,
304+
});
305+
});
306+
307+
it("should extract IDs from array of objects for ids filters", () => {
308+
// GIVEN
309+
const filters: Filter[] = [{ name: "related__ids", value: [{ id: "1" }, { id: "2" }] }];
310+
311+
// WHEN
312+
const result = addFiltersToRequest(filters);
313+
314+
// THEN
315+
expect(result).toEqual({
316+
related__ids: ["1", "2"],
317+
});
318+
});
319+
320+
it("should correctly combine multiple filters of different types", () => {
321+
// GIVEN
322+
const filters: Filter[] = [
323+
{ name: "name__value", value: "test" },
324+
{ name: "field__isnull", value: true },
325+
{ name: "related__ids", value: [{ id: "1" }] },
326+
];
327+
328+
// WHEN
329+
const result = addFiltersToRequest(filters);
330+
331+
// THEN
332+
expect(result).toEqual({
333+
partial_match: true,
334+
name__value: "test",
335+
field__isnull: true,
336+
related__ids: ["1"],
337+
});
338+
});
339+
340+
it("should return empty object for filters with invalid field name format", () => {
341+
// GIVEN
342+
const filters: Filter[] = [
343+
{ name: "invalid", value: "test" } as any,
344+
{ name: "also__invalid", value: "test" },
345+
];
346+
347+
// WHEN
348+
const result = addFiltersToRequest(filters);
349+
350+
// THEN
351+
expect(result).toEqual({});
352+
});
353+
});

frontend/app/src/shared/api/graphql/utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ATTRIBUTE_KIND } from "@/entities/schema/constants";
22
import { AttributeSchema, RelationshipSchema } from "@/entities/schema/types";
3+
import { Filter } from "@/shared/hooks/useFilters";
34

45
type AddAttributesToRequestOptions = {
56
withMetadata?: boolean;
@@ -102,3 +103,39 @@ export const addRelationshipsToRequest = (
102103
};
103104
}, {});
104105
};
106+
107+
export const addFiltersToRequest = (filters: Array<Filter>) => {
108+
return filters.reduce(
109+
(acc, filter) => {
110+
// Skip kind__value filter as it's handled separately
111+
if (filter.name === "kind__value") {
112+
return acc;
113+
}
114+
115+
const [fieldName, fieldKey] = filter.name.split("__");
116+
if (!fieldName || !fieldKey) {
117+
return acc;
118+
}
119+
120+
switch (fieldKey) {
121+
case "value":
122+
case "values": {
123+
acc.partial_match = true; // Add partial_match for text-based filters
124+
acc[filter.name] = filter.value;
125+
break;
126+
}
127+
case "isnull": {
128+
acc[filter.name] = filter.value;
129+
break;
130+
}
131+
case "ids": {
132+
acc[filter.name] = filter.value.map(({ id }: { id: string }) => id);
133+
break;
134+
}
135+
}
136+
137+
return acc;
138+
},
139+
{} as Record<string, string | number | boolean | string[]>
140+
);
141+
};

frontend/app/src/shared/components/inputs/enum.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,26 @@ export interface EnumProps {
154154
schema?: IModelSchema;
155155
className?: string;
156156
onChange: (value: string | number | null) => void;
157+
defaultOpen?: boolean;
158+
fitTriggerWidth?: boolean;
157159
}
158160

159161
export const Enum = forwardRef<HTMLButtonElement, EnumProps>(
160-
({ items, value, fieldSchema, schema, onChange, ...props }, ref) => {
162+
(
163+
{
164+
items,
165+
value,
166+
fieldSchema,
167+
schema,
168+
onChange,
169+
defaultOpen = false,
170+
fitTriggerWidth = false,
171+
...props
172+
},
173+
ref
174+
) => {
161175
const [localItems, setLocalItems] = useState(items);
162-
const [open, setOpen] = useState(false);
176+
const [open, setOpen] = useState(defaultOpen);
163177

164178
const handleAddOption = (newOption: string | number) => {
165179
setLocalItems([...localItems, newOption]);
@@ -179,7 +193,7 @@ export const Enum = forwardRef<HTMLButtonElement, EnumProps>(
179193
{value}
180194
</ComboboxTrigger>
181195

182-
<ComboboxContent>
196+
<ComboboxContent fitTriggerWidth={fitTriggerWidth}>
183197
<ComboboxList>
184198
<ComboboxEmpty>No enum found.</ComboboxEmpty>
185199
{localItems.map((item) => (

frontend/app/tests/e2e/objects/object-filters.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,29 @@ test.describe("Object filters", () => {
121121
await expect(page.getByTestId("object-items")).toContainText("Interface L3");
122122
});
123123
});
124+
125+
test("should filter using enum value", async ({ page }) => {
126+
await page.goto("/objects/InfraBGPSession");
127+
await expect(page.getByTestId("object-items")).toContainText("EXTERNAL");
128+
await expect(page.getByTestId("object-items")).toContainText("INTERNAL");
129+
130+
await page.getByRole("button", { name: "Type" }).click();
131+
await expect(page.getByPlaceholder("Filter...")).toBeFocused();
132+
await expect(page.getByRole("option", { name: "EXTERNAL" })).toBeVisible();
133+
await expect(page.getByRole("option", { name: "INTERNAL" })).toBeVisible();
134+
await page.getByRole("option", { name: "EXTERNAL" }).click();
135+
await expect(page.getByRole("combobox").filter({ hasText: "EXTERNAL" })).toBeVisible();
136+
await page.getByRole("button", { name: "Apply" }).click();
137+
138+
await expect(page.getByRole("row", { name: "Type contains EXTERNAL" })).toBeVisible();
139+
await expect(page.getByTestId("object-items")).toContainText("EXTERNAL");
140+
await expect(page.getByTestId("object-items")).not.toContainText("INTERNAL");
141+
142+
await page.getByRole("button", { name: "Type" }).click();
143+
await expect(page.getByRole("combobox").filter({ hasText: "EXTERNAL" })).toBeVisible();
144+
145+
await page.getByRole("row", { name: "Type contains EXTERNAL" }).click();
146+
await expect(page.getByTestId("object-items")).toContainText("EXTERNAL");
147+
await expect(page.getByTestId("object-items")).toContainText("INTERNAL");
148+
});
124149
});

0 commit comments

Comments
 (0)