Skip to content

Commit 267ca8e

Browse files
committed
feat: add empty/not empty map table filter
1 parent 2ad2c13 commit 267ca8e

5 files changed

Lines changed: 72 additions & 18 deletions

File tree

src/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,16 @@ export enum APIFilterOperator {
5555
* Types of filters that can be applied
5656
*/
5757
export enum APIFilterType {
58+
/** Empty value filter */
59+
EMPTY = "EMPTY",
5860
/** Exact match filter */
5961
EXACT = "EXACT",
6062
/** Geographic filter (proximity, turf) */
6163
GEO = "GEO",
6264
/** Multi-select filter */
6365
MULTI = "MULTI",
66+
/** Not empty value filter */
67+
NOT_EMPTY = "NOT_EMPTY",
6468
/** Text search filter */
6569
TEXT = "TEXT",
6670
}

src/app/map/[id]/components/table/MapTableFilter.tsx

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { usePlacedMarkersQuery } from "@/app/map/[id]/hooks/usePlacedMarkers";
88
import { useTable } from "@/app/map/[id]/hooks/useTable";
99
import { useTurfsQuery } from "@/app/map/[id]/hooks/useTurfsQuery";
1010
import MultiDropdownMenu from "@/components/MultiDropdownMenu";
11-
import { FilterOperator, FilterType } from "@/server/models/MapView";
11+
import { FilterTypeLabels } from "@/labels";
12+
import {
13+
FilterOperator,
14+
FilterType,
15+
columnFilterTypes,
16+
} from "@/server/models/MapView";
1217
import { useTRPC } from "@/services/trpc/react";
1318
import { Button } from "@/shadcn/ui/button";
1419
import {
@@ -353,27 +358,37 @@ function ChildFilter({ filter, setFilter }: TableFilterProps) {
353358
</span>
354359
<select
355360
aria-label="Filter type"
356-
value={filter.type === FilterType.EXACT ? "is" : "contains"}
357-
onChange={(e) =>
361+
value={filter.type}
362+
onChange={(e) => {
363+
const newType = e.target.value as FilterType;
364+
const needsSearch =
365+
newType !== FilterType.EMPTY && newType !== FilterType.NOT_EMPTY;
358366
updateFilter({
359-
type: e.target.value === "is" ? FilterType.EXACT : FilterType.TEXT,
360-
})
361-
}
367+
type: newType,
368+
search: needsSearch ? filter.search : undefined,
369+
});
370+
}}
362371
className="bg-neutral-100 text-sm cursor-pointer border-0 border-l text-center font-medium"
363372
>
364-
<option value="contains">contains</option>
365-
<option value="is">is</option>
373+
{columnFilterTypes.map((type) => (
374+
<option key={type} value={type}>
375+
{FilterTypeLabels[type]}
376+
</option>
377+
))}
366378
</select>
367-
<Input
368-
type="text"
369-
placeholder="Search"
370-
value={localSearch}
371-
onChange={(e) => onSearchChange(e.target.value)}
372-
style={{ width: `${Math.max(5, localSearch.length + 1)}ch` }}
373-
className="min-w-20 h-7 p-2 text-sm border-y-0 border-r-0 rounded-none bg-neutral-100 font-medium text-center"
374-
ref={inputRef}
375-
required
376-
/>
379+
{filter.type !== FilterType.EMPTY &&
380+
filter.type !== FilterType.NOT_EMPTY && (
381+
<Input
382+
type="text"
383+
placeholder="Search"
384+
value={localSearch}
385+
onChange={(e) => onSearchChange(e.target.value)}
386+
style={{ width: `${Math.max(5, localSearch.length + 1)}ch` }}
387+
className="min-w-20 h-7 p-2 text-sm border-y-0 border-r-0 rounded-none bg-neutral-100 font-medium text-center"
388+
ref={inputRef}
389+
required
390+
/>
391+
)}
377392
</div>
378393
);
379394
}

src/labels.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Human friendly labels for enums
22

3+
import { FilterType } from "./server/models/MapView";
34
import type { AreaSetCode, AreaSetGroupCode } from "./server/models/AreaSet";
45
import type {
56
DataSourceRecordType,
@@ -11,6 +12,7 @@ import type {
1112
googleSheetsConfigSchema,
1213
mailchimpConfigSchema,
1314
} from "./server/models/DataSource";
15+
import type { columnFilterTypes } from "./server/models/MapView";
1416
import type { DataSourceType } from "@/server/models/DataSource";
1517
import type z from "zod";
1618

@@ -44,6 +46,16 @@ export const AreaSetGroupCodeLabels: Record<AreaSetGroupCode, string> = {
4446
SENC22: "Senedd Constituency",
4547
};
4648

49+
export const FilterTypeLabels: Record<
50+
(typeof columnFilterTypes)[number],
51+
string
52+
> = {
53+
[FilterType.TEXT]: "contains",
54+
[FilterType.EXACT]: "is",
55+
[FilterType.EMPTY]: "is empty",
56+
[FilterType.NOT_EMPTY]: "is not empty",
57+
};
58+
4759
type DataSourceConfigKey =
4860
| keyof z.infer<typeof actionNetworkConfigSchema>
4961
| keyof z.infer<typeof airtableConfigSchema>

src/server/models/MapView.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ export enum FilterOperator {
1616
export const filterOperators = Object.values(FilterOperator);
1717

1818
export enum FilterType {
19+
EMPTY = "EMPTY",
1920
EXACT = "EXACT",
2021
GEO = "GEO",
2122
MULTI = "MULTI",
23+
NOT_EMPTY = "NOT_EMPTY",
2224
TEXT = "TEXT",
2325
}
2426

2527
export const filterTypes = Object.values(FilterType);
2628

29+
export const columnFilterTypes = [
30+
FilterType.TEXT,
31+
FilterType.EXACT,
32+
FilterType.EMPTY,
33+
FilterType.NOT_EMPTY,
34+
] as const;
35+
2736
const baseRecordFilterSchema = z.object({
2837
column: z.string().nullish(),
2938
dataRecordId: z.string().nullish(),

src/server/repositories/DataRecord.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ export function applyFilterAndSearch(
105105
);
106106
}
107107

108+
if (filter?.type === FilterType.EMPTY && filter.column) {
109+
return eb.or([
110+
eb(eb.ref("json", "->>").key(filter.column), "is", null),
111+
eb(eb.ref("json", "->>").key(filter.column), "=", ""),
112+
]);
113+
}
114+
115+
if (filter?.type === FilterType.NOT_EMPTY && filter.column) {
116+
return eb.and([
117+
eb(eb.ref("json", "->>").key(filter.column), "is not", null),
118+
eb(eb.ref("json", "->>").key(filter.column), "!=", ""),
119+
]);
120+
}
121+
108122
// Trivially always true fallthrough expression for reliability
109123
// (e.g. in the case of a broken filter, return everything rather than nothing)
110124
return eb(eb.val(1), "=", 1);

0 commit comments

Comments
 (0)