Skip to content

Commit f58674f

Browse files
committed
[Filters] boolean filters + debug missing value
- Terms filters can be now used on category, keywords OR boolean - keepMissingValues is not usable for category or boolean terms filter - refacto Select components to allow generic value type for options fixes #275 #276
1 parent 46534d9 commit f58674f

File tree

9 files changed

+112
-58
lines changed

9 files changed

+112
-58
lines changed

packages/gephi-lite/src/components/GraphFilters/TermsFilter.tsx

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,66 @@
11
import { countBy, flatMap, identity, sortBy } from "lodash";
2-
import { FC, useEffect, useState } from "react";
2+
import { FC, useCallback, useEffect, useState } from "react";
33
import { useTranslation } from "react-i18next";
44

55
import { useFiltersActions, useGraphDataset } from "../../core/context/dataContexts";
66
import { TermsFilterType } from "../../core/filters/types";
77
import { useFilteredGraphAt } from "../../core/graph";
88
import { computeAllDynamicAttributes, mergeStaticDynamicData } from "../../core/graph/dynamicAttributes";
99
import { getFieldValue } from "../../core/graph/fieldModel";
10-
import { Select } from "../forms/Select";
10+
import { BaseOption, Select } from "../forms/Select";
1111
import { toPairsCompatibleWithSymbol } from "./utils";
1212

1313
const unavailableValue: unique symbol = Symbol("Not Available");
14+
const trueValue: unique symbol = Symbol("True Value");
15+
const falseValue: unique symbol = Symbol("False Value");
16+
17+
type TermsFilterSymbolsType = typeof unavailableValue | typeof trueValue | typeof falseValue;
18+
const symbolToValue = (symbol: TermsFilterSymbolsType): boolean | null => {
19+
switch (symbol) {
20+
case unavailableValue:
21+
return null;
22+
case trueValue:
23+
return true;
24+
case falseValue:
25+
return false;
26+
}
27+
};
28+
const valueToSymbol = (value: string | boolean | null): string | TermsFilterSymbolsType => {
29+
switch (typeof value) {
30+
case "string":
31+
return value;
32+
case "boolean":
33+
return value === true ? trueValue : falseValue;
34+
default:
35+
return unavailableValue;
36+
}
37+
};
1438

1539
export const TermsFilter: FC<{ filter: TermsFilterType; filterIndex: number }> = ({ filter, filterIndex }) => {
1640
const parentGraph = useFilteredGraphAt(filterIndex - 1);
1741
const { nodeData, edgeData } = useGraphDataset();
1842

1943
const { t } = useTranslation();
44+
const termLabel = useCallback(
45+
(term: string | TermsFilterSymbolsType) => {
46+
switch (term) {
47+
case unavailableValue:
48+
return t("filters.noValueOption");
49+
case trueValue:
50+
return t("filters.booleanTrueOption");
51+
case falseValue:
52+
return t("filters.booleanFalseOption");
53+
default:
54+
return term;
55+
}
56+
},
57+
[t],
58+
);
2059
const { updateFilter } = useFiltersActions();
21-
const [dataTerms, setDataTerms] = useState<Record<string | typeof unavailableValue, number>>({
60+
const [dataTerms, setDataTerms] = useState<Record<string | TermsFilterSymbolsType, number>>({
2261
[unavailableValue]: 0,
62+
[trueValue]: 0,
63+
[falseValue]: 0,
2364
});
2465

2566
useEffect(() => {
@@ -35,36 +76,35 @@ export const TermsFilter: FC<{ filter: TermsFilterType; filterIndex: number }> =
3576
filter.itemType === "nodes" ? parentGraph.nodes() : parentGraph.edges(),
3677
(itemId) => {
3778
const fieldValue = getFieldValue(itemData[itemId], filter.field);
38-
if (fieldValue === undefined && filter.field.type === "category")
79+
if (fieldValue === undefined && (filter.field.type === "category" || filter.field.type === "boolean"))
3980
// if fieldValue is undefined we return the NA symbol but only for category field
4081
return unavailableValue;
41-
else return fieldValue;
82+
if (filter.field.type === "boolean") return fieldValue === true ? trueValue : falseValue;
83+
return fieldValue;
4284
},
4385
// for category field we keep notAvailable values to propose it a possible filter value
44-
).filter((v) => (filter.field.type !== "category" ? typeof v === "string" : true)),
86+
).filter((v) =>
87+
filter.field.type !== "category" && filter.field.type !== "boolean" ? typeof v === "string" : true,
88+
),
4589
identity,
4690
);
4791
setDataTerms(terms as Record<string | symbol, number>);
4892
}, [filter, parentGraph, nodeData, edgeData]);
49-
console.log(filter.terms);
93+
5094
return (
5195
<div className="w-100">
52-
<Select
96+
<Select<BaseOption<string | null | boolean>, true>
5397
autoFocus
5498
value={
5599
filter.terms
56100
? Array.from(filter.terms).map((term) => ({
57-
label: term === null ? t("filters.noValueOption") : term,
101+
label: termLabel(valueToSymbol(term)),
58102
value: term,
59103
}))
60104
: []
61105
}
62106
onChange={(options) => {
63-
const selectedValues = new Set(
64-
options.map((o): string | null =>
65-
o.value === unavailableValue || typeof o.value === "symbol" ? null : o.value,
66-
),
67-
);
107+
const selectedValues = new Set(options.map((o): string | null | boolean => o.value));
68108
updateFilter(filterIndex, {
69109
...filter,
70110
terms: selectedValues.size > 0 ? selectedValues : undefined,
@@ -77,18 +117,18 @@ export const TermsFilter: FC<{ filter: TermsFilterType; filterIndex: number }> =
77117
}}
78118
options={sortBy(toPairsCompatibleWithSymbol(dataTerms), ([_term, nbOcc]) => -1 * nbOcc).map(([term, nbOcc]) => {
79119
return {
80-
label: `${typeof term === "symbol" ? t("filters.noValueOption") : term} (${nbOcc} ${t(`graph.model.${filter.itemType}`)})`,
81-
value: term === unavailableValue ? null : term,
120+
label: `${termLabel(term as string | TermsFilterSymbolsType)} (${nbOcc} ${t(`graph.model.${filter.itemType}`)})`,
121+
value: typeof term === "string" ? term : symbolToValue(term as TermsFilterSymbolsType),
82122
};
83123
})}
84124
/>
85-
{filter.field.type !== "category" && (
125+
{filter.field.type !== "category" && filter.field.type !== "boolean" && (
86126
<div className="form-check mt-1">
87127
<input
88128
className="form-check-input"
89129
type="checkbox"
90130
id="keepMissingValuesTerms"
91-
checked={!!filter.keepMissingValues}
131+
checked={filter.keepMissingValues === true}
92132
onChange={(e) => {
93133
updateFilter(filterIndex, { ...filter, keepMissingValues: e.target.checked });
94134
}}

packages/gephi-lite/src/components/data/Attribute.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import ColorPicker, { InlineColorPicker } from "../ColorPicker";
2424
import MessageTooltip from "../MessageTooltip";
2525
import { FieldModelIcon, InvalidDataIcon } from "../common-icons";
2626
import { Checkbox } from "../forms/Checkbox";
27-
import { BaseOption, CreatableSelect, optionize } from "../forms/Select";
27+
import { CreatableSelect, StringOption, optionize } from "../forms/Select";
2828

2929
/**
3030
* Render values:
@@ -191,8 +191,8 @@ export const AttributeEditors: {
191191
.flatMap((v) => (isNil(v) ? [] : [optionize(v)])),
192192
[values],
193193
);
194-
const OptionComponent = useCallback((props: OptionProps<BaseOption, false>) => {
195-
const Option = components.Option<BaseOption, false, GroupBase<BaseOption>>;
194+
const OptionComponent = useCallback((props: OptionProps<StringOption, false>) => {
195+
const Option = components.Option<StringOption, false, GroupBase<StringOption>>;
196196
return (
197197
<div
198198
onClick={(e) => {
@@ -205,8 +205,8 @@ export const AttributeEditors: {
205205
</div>
206206
);
207207
}, []);
208-
const SingleValueComponent = useCallback((props: SingleValueProps<BaseOption, false>) => {
209-
const SingleValue = components.SingleValue<BaseOption, false, GroupBase<BaseOption>>;
208+
const SingleValueComponent = useCallback((props: SingleValueProps<StringOption, false>) => {
209+
const SingleValue = components.SingleValue<StringOption, false, GroupBase<StringOption>>;
210210
return (
211211
<SingleValue {...props}>
212212
<RenderCategory value={props.data.value} />
@@ -215,7 +215,7 @@ export const AttributeEditors: {
215215
}, []);
216216

217217
return (
218-
<CreatableSelect<BaseOption>
218+
<CreatableSelect<StringOption>
219219
id={id}
220220
autoFocus={autoFocus}
221221
menuPosition="absolute"
@@ -241,8 +241,8 @@ export const AttributeEditors: {
241241
[values],
242242
);
243243
const OptionComponent = useCallback(
244-
(props: OptionProps<BaseOption, true>) => {
245-
const Option = components.Option<BaseOption, true, GroupBase<BaseOption>>;
244+
(props: OptionProps<StringOption, true>) => {
245+
const Option = components.Option<StringOption, true, GroupBase<StringOption>>;
246246
return (
247247
<div
248248
onClick={(e) => {
@@ -258,8 +258,8 @@ export const AttributeEditors: {
258258
[field.separator],
259259
);
260260
const MultiValueContainerComponent = useCallback(
261-
(props: MultiValueProps<BaseOption, true>) => {
262-
const MultiValueContainer = components.MultiValueContainer<BaseOption, true, GroupBase<BaseOption>>;
261+
(props: MultiValueProps<StringOption, true>) => {
262+
const MultiValueContainer = components.MultiValueContainer<StringOption, true, GroupBase<StringOption>>;
263263
return (
264264
<MultiValueContainer {...props}>
265265
<RenderKeywords value={[props.data.value]} separator={field.separator} />
@@ -270,13 +270,13 @@ export const AttributeEditors: {
270270
);
271271

272272
return (
273-
<CreatableSelect<BaseOption, true>
273+
<CreatableSelect<StringOption, true>
274274
isMulti
275275
id={id}
276276
autoFocus={autoFocus}
277277
menuPosition="absolute"
278278
placeholder={placeholder}
279-
value={value?.map(optionize)}
279+
value={value?.map(optionize<string>)}
280280
onChange={(newValue) => onChange(newValue.length ? newValue.map((o) => o.value) : undefined)}
281281
options={options}
282282
isClearable

packages/gephi-lite/src/components/forms/Select.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,27 @@ const useDefaultSelectProps = () => {
2828
};
2929
};
3030

31-
export interface BaseOption<T extends string = string> {
32-
value: T;
31+
export interface BaseOption<V> {
32+
value: V;
3333
label: ReactNode;
3434
}
3535

36+
export type StringOption = BaseOption<string>;
37+
3638
export function optionize(value: undefined): undefined;
37-
export function optionize<T extends string = string>(value: T): BaseOption<T>;
38-
export function optionize<T extends string = string>(value?: T): BaseOption<T> | undefined {
39-
return !isNil(value) ? { value, label: value } : undefined;
39+
export function optionize<V>(value: V): BaseOption<V>;
40+
export function optionize<V>(value?: V): BaseOption<V> | undefined {
41+
return !isNil(value) ? { value, label: value + "" } : undefined;
4042
}
4143

42-
export function Select<T = BaseOption, IsMulti extends boolean = false>({
44+
export function Select<BO, IsMulti extends boolean = false>({
4345
ref,
4446
...props
45-
}: Props<T, IsMulti> & { ref?: LegacyRef<SelectInstance<T, IsMulti>> }) {
47+
}: Props<BO, IsMulti> & { ref?: LegacyRef<SelectInstance<BO, IsMulti>> }) {
4648
const { portalTarget } = useContext(UIContext);
4749
const defaultProps = useDefaultSelectProps();
4850
return (
49-
<ReactSelect<T, IsMulti>
51+
<ReactSelect<BO, IsMulti>
5052
menuPortalTarget={portalTarget}
5153
{...defaultProps}
5254
{...props}
@@ -59,14 +61,14 @@ export function Select<T = BaseOption, IsMulti extends boolean = false>({
5961
);
6062
}
6163

62-
export function AsyncSelect<T = BaseOption, IsMulti extends boolean = false>({
64+
export function AsyncSelect<BO, IsMulti extends boolean = false>({
6365
ref,
6466
...props
65-
}: AsyncProps<T, IsMulti, GroupBase<T>> & { ref?: LegacyRef<SelectInstance<T, IsMulti>> }) {
67+
}: AsyncProps<BO, IsMulti, GroupBase<BO>> & { ref?: LegacyRef<SelectInstance<BO, IsMulti>> }) {
6668
const { portalTarget } = useContext(UIContext);
6769
const defaultProps = useDefaultSelectProps();
6870
return (
69-
<AsyncReactSelect<T, IsMulti>
71+
<AsyncReactSelect<BO, IsMulti>
7072
menuPortalTarget={portalTarget}
7173
{...defaultProps}
7274
{...props}
@@ -79,14 +81,14 @@ export function AsyncSelect<T = BaseOption, IsMulti extends boolean = false>({
7981
);
8082
}
8183

82-
export function CreatableSelect<T = BaseOption, IsMulti extends boolean = false>({
84+
export function CreatableSelect<BO, IsMulti extends boolean = false>({
8385
ref,
8486
...props
85-
}: CreatableProps<T, IsMulti, GroupBase<T>> & { ref?: LegacyRef<SelectInstance<T, IsMulti>> }) {
87+
}: CreatableProps<BO, IsMulti, GroupBase<BO>> & { ref?: LegacyRef<SelectInstance<BO, IsMulti>> }) {
8688
const { portalTarget } = useContext(UIContext);
8789
const defaultProps = useDefaultSelectProps();
8890
return (
89-
<CreatableReactSelect<T, IsMulti>
91+
<CreatableReactSelect<BO, IsMulti>
9092
menuPortalTarget={portalTarget}
9193
{...defaultProps}
9294
{...props}
@@ -99,14 +101,14 @@ export function CreatableSelect<T = BaseOption, IsMulti extends boolean = false>
99101
);
100102
}
101103

102-
export function AsyncCreatableSelect<T = BaseOption, IsMulti extends boolean = false>({
104+
export function AsyncCreatableSelect<BO, IsMulti extends boolean = false>({
103105
ref,
104106
...props
105-
}: AsyncCreatableProps<T, IsMulti, GroupBase<T>> & { ref?: LegacyRef<SelectInstance<T, IsMulti>> }) {
107+
}: AsyncCreatableProps<BO, IsMulti, GroupBase<BO>> & { ref?: LegacyRef<SelectInstance<BO, IsMulti>> }) {
106108
const { portalTarget } = useContext(UIContext);
107109
const defaultProps = useDefaultSelectProps();
108110
return (
109-
<AsyncCreatableReactSelect<T, IsMulti>
111+
<AsyncCreatableReactSelect<BO, IsMulti>
110112
menuPortalTarget={portalTarget}
111113
{...defaultProps}
112114
{...props}

packages/gephi-lite/src/components/modals/SelectFilterModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const FILTER_TYPES_PER_FIELD_TYPES: Record<FieldModelType, "range" | "terms" | n
1919
number: "range",
2020
keywords: "terms",
2121
category: "terms",
22-
boolean: null,
22+
boolean: "terms",
2323
color: null,
2424
text: null,
2525
url: null,
@@ -104,7 +104,7 @@ const SelectFilterModal: FC<
104104
itemType: type,
105105
type: filterType,
106106
field,
107-
keepMissingValues: true,
107+
keepMissingValues: field.type !== "category",
108108
});
109109
}}
110110
>

packages/gephi-lite/src/core/filters/utils.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ export function filterValue(
2929
if (scalar === undefined || scalar === null) {
3030
// we keep missing values if user specifically asked for them
3131
return (
32+
// dedicated filter
3233
filter.type === "missingValue" ||
33-
(filter.type === "terms" && filter.terms?.has(null)) ||
34-
!!filter.keepMissingValues
34+
// terms filter on category has a special explicit choice for including missing values
35+
// we keep missing value if no choice has been made yet (empty filter) or if the special case has been selected
36+
(filter.type === "terms" &&
37+
["category", "boolean"].includes(filter.field.type) &&
38+
(filter.terms === undefined || filter.terms.size === 0 || filter.terms?.has(null))) ||
39+
// for all other filter we have a dedicated special settings for that
40+
filter.keepMissingValues === true
3541
);
3642
}
3743

@@ -55,10 +61,10 @@ export function filterValue(
5561
if (value instanceof DateTime || isNumber(value)) {
5662
return !!filter.keepMissingValues;
5763
}
58-
const strings = (Array.isArray(value) ? value : !isNil(value) ? [value] : []).filter(
59-
(v): v is string => typeof v === "string",
64+
const stringsOrBoolean = (Array.isArray(value) ? value : !isNil(value) ? [value] : []).filter(
65+
(v): v is string => ["string", "boolean"].includes(typeof v),
6066
);
61-
return strings.some((string) => !filter.terms || filter.terms.has(string));
67+
return stringsOrBoolean.some((stringOrBoolean) => !filter.terms || filter.terms.has(stringOrBoolean));
6268
}
6369
}
6470
case "missingValue":

packages/gephi-lite/src/locales/dev.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,9 @@
329329
},
330330
"visible_graph": "Visible graph",
331331
"missingValues": "Missing values",
332-
"noValueOption": "No value"
332+
"noValueOption": "No value",
333+
"booleanTrueOption": "true",
334+
"booleanFalseOption": "false"
333335
},
334336
"gephi-lite": {
335337
"open_welcome_modal": "Open welcome modal",

packages/gephi-lite/src/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@
220220
}
221221
},
222222
"missingValues": "Missing values",
223-
"noValueOption": "No value"
223+
"noValueOption": "No value",
224+
"booleanTrueOption": "true",
225+
"booleanFalseOption": "false"
224226
},
225227
"gephi-lite": {
226228
"info": "More info on Gephi Lite",

packages/gephi-lite/src/locales/fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,9 @@
361361
}
362362
},
363363
"using": "Utilisation",
364-
"visible_graph": "Réseau visible"
364+
"visible_graph": "Réseau visible",
365+
"booleanTrueOption": "vrai",
366+
"booleanFalseOption": "faux"
365367
},
366368
"gephi-lite": {
367369
"info": "Plus d'informations sur Gephi Lite",

packages/sdk/src/filters/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type RangeFilterType = BaseFilter & {
1616
export interface TermsFilterType extends BaseFilter {
1717
type: "terms";
1818
itemType: ItemType;
19-
terms?: Set<string | null>;
19+
terms?: Set<string | boolean | null>;
2020
keepMissingValues?: boolean;
2121
}
2222

0 commit comments

Comments
 (0)