Skip to content

Commit ace7bc3

Browse files
committed
chore: Merge main
2 parents c357e93 + 75fd5d8 commit ace7bc3

22 files changed

Lines changed: 466 additions & 611 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ You can also check the [release page](https://github.com/visualize-admin/visuali
1111

1212
Nothing yet.
1313

14+
## [3.14.0] - 2022-11-29
15+
16+
- Add search to filters with hierarchy
17+
- Move interactive filters in place
18+
- Diminish banner height in search
19+
- Increase depth limit of hierarchies to 6
20+
1421
## [3.13.0] - 2022-11-22
1522

1623
- Enhancements:

app/charts/column/columns-state.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ import {
5959
TimeUnit,
6060
} from "@/graphql/query-hooks";
6161
import { getPalette } from "@/palettes";
62-
import { makeDimensionValueSorters } from "@/utils/sorting-values";
62+
import {
63+
getSortingOrders,
64+
makeDimensionValueSorters,
65+
} from "@/utils/sorting-values";
6366

6467
export interface ColumnsState extends CommonChartState {
6568
chartType: "column";
@@ -171,7 +174,14 @@ const useColumnsState = (
171174
const { xScale, yScale, xEntireScale, xScaleInteraction, bandDomain } =
172175
useMemo(() => {
173176
// x
174-
const bandDomain = [...new Set(preparedData.map(getX))];
177+
const sorters = makeDimensionValueSorters(xDimension, {
178+
sorting: fields.x.sorting,
179+
});
180+
const bandDomain = orderBy(
181+
[...new Set(preparedData.map(getX))],
182+
sorters,
183+
getSortingOrders(sorters, fields.x.sorting)
184+
);
175185
const xScale = scaleBand()
176186
.domain(bandDomain)
177187
.paddingInner(PADDING_INNER)

app/charts/shared/chart-data-filters.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
OptionGroup,
1717
Option,
1818
} from "@/configurator";
19-
import { hierarchyToOptions, TimeInput } from "@/configurator/components/field";
19+
import { TimeInput } from "@/configurator/components/field";
2020
import {
2121
getTimeIntervalFormattedSelectOptions,
2222
getTimeIntervalWithProps,
@@ -35,6 +35,7 @@ import {
3535
} from "@/graphql/query-hooks";
3636
import { Icon } from "@/icons";
3737
import { useLocale } from "@/locales/use-locale";
38+
import { hierarchyToOptions } from "@/utils/hierarchy";
3839

3940
export const ChartDataFilters = ({
4041
dataSet,

app/components/form.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
import { useBrowseContext } from "@/configurator/components/dataset-browse";
4343
import { Icon } from "@/icons";
4444
import { useLocale } from "@/locales/use-locale";
45+
import { valueComparator } from "@/utils/sorting-values";
4546

4647
export const Label = ({
4748
htmlFor,
@@ -238,13 +239,7 @@ const getSelectOptions = (
238239
const restOptions = options.filter((o) => !o.isNoneValue);
239240

240241
if (sortOptions) {
241-
restOptions.sort((a, b) => {
242-
if (a.position !== undefined && b.position !== undefined) {
243-
return a.position < b.position;
244-
} else {
245-
return a.label.localeCompare(b.label, locale);
246-
}
247-
});
242+
restOptions.sort(valueComparator(locale));
248243
}
249244

250245
return [...noneOptions, ...restOptions];

app/components/select-tree.tsx

Lines changed: 146 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,30 @@ import MUITreeItem, {
66
TreeItemProps,
77
useTreeItem,
88
} from "@mui/lab/TreeItem";
9-
import TreeView from "@mui/lab/TreeView";
9+
import TreeView, { TreeViewProps } from "@mui/lab/TreeView";
1010
import {
1111
Theme,
12-
Menu,
12+
Popover,
1313
PopoverActions,
1414
useEventCallback,
1515
OutlinedInput,
1616
Typography,
1717
Collapse,
18+
TextField,
19+
TextFieldProps,
20+
IconButton,
1821
} from "@mui/material";
1922
import { makeStyles } from "@mui/styles";
2023
import useId from "@mui/utils/useId";
2124
import clsx from "clsx";
22-
import { useCallback, useMemo, useState } from "react";
25+
import { useCallback, useMemo, useRef, useState } from "react";
2326
import * as React from "react";
2427
import { useEffect } from "react";
2528

2629
import { Label } from "@/components/form";
30+
import { HierarchyValue } from "@/graphql/resolver-types";
2731
import { Icon } from "@/icons";
32+
import { flattenTree, pruneTree } from "@/rdf/tree-utils";
2833
import useEvent from "@/utils/use-event";
2934

3035
const useStyles = makeStyles<Theme, { disabled?: boolean; open?: boolean }>(
@@ -242,12 +247,15 @@ const TreeItem = (props: TreeItemProps) => {
242247
return <MUITreeItem {...props} ContentComponent={TreeItemContent} />;
243248
};
244249

245-
type Tree = {
246-
value: string;
247-
label: string;
248-
children?: Tree;
250+
type TreeHierachyValue = Omit<
251+
HierarchyValue,
252+
"depth" | "dimensionIri" | "children"
253+
> & {
249254
selectable?: boolean;
250-
}[];
255+
children?: TreeHierachyValue[];
256+
};
257+
258+
type Tree = TreeHierachyValue[];
251259

252260
type NodeId = string;
253261

@@ -263,6 +271,16 @@ export type SelectTreeProps = {
263271
open?: boolean;
264272
};
265273

274+
const getFilteredOptions = (options: Tree, value: string) => {
275+
const rx = new RegExp(`^${value}|\\s${value}`, "i");
276+
return value === ""
277+
? options
278+
: (pruneTree(
279+
options as HierarchyValue[],
280+
(d) => !!d.label.match(rx)
281+
) as Tree);
282+
};
283+
266284
function SelectTree({
267285
label,
268286
options,
@@ -276,20 +294,29 @@ function SelectTree({
276294
}: SelectTreeProps) {
277295
const [openState, setOpenState] = useState(false);
278296
const [minMenuWidth, setMinMenuWidth] = useState<number>();
279-
280-
const handleClick = useEventCallback((ev: React.MouseEvent<HTMLElement>) => {
281-
setOpenState(true);
282-
setMinMenuWidth(ev.currentTarget.clientWidth);
283-
onOpen?.();
284-
});
285-
286-
const handleClose = useEventCallback(() => {
287-
setOpenState(false);
288-
onClose?.();
289-
});
297+
const [inputValue, setInputValue] = useState("");
290298
const classes = useStyles({ disabled, open });
299+
const [filteredOptions, setFilteredOptions] = useState(options);
291300

292301
const parentsRef = React.useRef({} as Record<NodeId, NodeId>);
302+
const menuRef = React.useRef<PopoverActions>(null);
303+
const id = useId();
304+
const inputRef = React.useRef<HTMLDivElement>();
305+
306+
const defaultExpanded = useMemo(() => {
307+
if (!value && options.length > 0) {
308+
return options[0].value ? [options[0].value] : [];
309+
}
310+
const res = value ? [value] : [];
311+
let cur = value;
312+
const parents = parentsRef.current;
313+
while (cur && parents[cur]) {
314+
res.push(parents[cur]);
315+
cur = parents[cur];
316+
}
317+
return res;
318+
}, [value, options]);
319+
const [expanded, setExpanded] = useState(defaultExpanded);
293320

294321
const labelsByValue = useMemo(() => {
295322
parentsRef.current = {} as Record<NodeId, NodeId>;
@@ -309,20 +336,50 @@ function SelectTree({
309336
return res;
310337
}, [options]);
311338

312-
const defaultExpanded = useMemo(() => {
313-
if (!value && options.length > 0) {
314-
return options[0].value ? [options[0].value] : [];
315-
}
316-
const res = value ? [value] : [];
317-
let cur = value;
318-
const parents = parentsRef.current;
319-
while (cur && parents[cur]) {
320-
res.push(parents[cur]);
321-
cur = parents[cur];
322-
}
339+
const handleOpen = useEventCallback(() => {
340+
setOpenState(true);
341+
setMinMenuWidth(inputRef.current?.clientLeft);
342+
onOpen?.();
343+
});
323344

324-
return res;
325-
}, [value, options]);
345+
const handleClose = useEventCallback(() => {
346+
setOpenState(false);
347+
setInputValue("");
348+
setFilteredOptions(getFilteredOptions(options, ""));
349+
setExpanded(defaultExpanded);
350+
onClose?.();
351+
});
352+
353+
const handleInputChange: TextFieldProps["onChange"] = useEvent((ev) => {
354+
const value = ev.currentTarget.value;
355+
setInputValue(value);
356+
const filteredOptions = getFilteredOptions(options, value);
357+
setFilteredOptions(filteredOptions);
358+
setExpanded((curExpanded) => {
359+
const newExpanded = Array.from(
360+
new Set([
361+
...curExpanded,
362+
...flattenTree(filteredOptions as HierarchyValue[]).map(
363+
(v) => v.value
364+
),
365+
])
366+
);
367+
return newExpanded;
368+
});
369+
});
370+
371+
const handleClickResetInput = useEvent(() => {
372+
const newValue = "";
373+
setInputValue(newValue);
374+
setFilteredOptions(getFilteredOptions(options, newValue));
375+
setExpanded(defaultExpanded);
376+
});
377+
378+
const handleNodeToggle: TreeViewProps["onNodeToggle"] = useEvent(
379+
(_ev, nodeIds) => {
380+
setExpanded(nodeIds);
381+
}
382+
);
326383

327384
const handleNodeSelect = useEventCallback((_ev, value: NodeId) => {
328385
onChange({ target: { value: value } });
@@ -375,15 +432,14 @@ function SelectTree({
375432
[defaultExpanded, treeItemClasses, treeItemTransitionProps]
376433
);
377434

378-
const menuRef = React.useRef<PopoverActions>(null);
379-
380-
const paperProps = useMemo(() => {
381-
return {
435+
const paperProps = useMemo(
436+
() => ({
382437
style: {
383-
minWidth: minMenuWidth === undefined ? 0 : minMenuWidth,
438+
minWidth: minMenuWidth ?? 0,
384439
},
385-
};
386-
}, [minMenuWidth]);
440+
}),
441+
[minMenuWidth]
442+
);
387443

388444
const menuTransitionProps = useMemo(
389445
() => ({
@@ -401,8 +457,14 @@ function SelectTree({
401457
[]
402458
);
403459

404-
const id = useId();
405-
const inputRef = React.useRef<HTMLDivElement>();
460+
const treeRef = useRef();
461+
const handleKeyDown: React.HTMLAttributes<HTMLInputElement>["onKeyDown"] =
462+
useEvent((ev) => {
463+
if (ev.key === "Enter" || ev.key == " ") {
464+
handleOpen();
465+
ev.preventDefault();
466+
}
467+
});
406468

407469
useEffect(() => {
408470
const inputNode = inputRef.current;
@@ -426,10 +488,11 @@ function SelectTree({
426488
ref={inputRef}
427489
size="small"
428490
className={classes.input}
429-
onClick={disabled ? undefined : handleClick}
491+
onClick={disabled ? undefined : handleOpen}
492+
onKeyDown={handleKeyDown}
430493
endAdornment={<Icon className={classes.icon} name="caretDown" />}
431494
/>
432-
<Menu
495+
<Popover
433496
anchorOrigin={{
434497
vertical: "bottom",
435498
horizontal: "left",
@@ -441,17 +504,46 @@ function SelectTree({
441504
PaperProps={paperProps}
442505
TransitionProps={menuTransitionProps}
443506
>
444-
<TreeView
445-
defaultSelected={value}
446-
defaultExpanded={defaultExpanded}
447-
onNodeSelect={handleNodeSelect}
448-
defaultCollapseIcon={<ExpandMoreIcon />}
449-
defaultExpandIcon={<ChevronRightIcon />}
450-
sx={{ flexGrow: 1, overflowY: "auto", pb: 2, "user-select": "none" }}
451-
>
452-
{renderTreeContent(options)}
453-
</TreeView>
454-
</Menu>
507+
<TextField
508+
size="small"
509+
value={inputValue}
510+
sx={{ p: 1, width: "100%" }}
511+
InputProps={{
512+
autoFocus: true,
513+
startAdornment: <Icon name="search" size={16} color="#555" />,
514+
endAdornment: (
515+
<IconButton size="small" onClick={handleClickResetInput}>
516+
<Icon name="close" size={16} />
517+
</IconButton>
518+
),
519+
sx: { "& input": { fontSize: "12px" }, pl: 1, pr: 1 },
520+
}}
521+
onChange={handleInputChange}
522+
/>
523+
{filteredOptions.length === 0 ? (
524+
<Typography variant="body2" sx={{ px: 2, py: 4 }}>
525+
<Trans id="No results" />
526+
</Typography>
527+
) : (
528+
<TreeView
529+
ref={treeRef}
530+
defaultSelected={value}
531+
expanded={expanded}
532+
onNodeToggle={handleNodeToggle}
533+
onNodeSelect={handleNodeSelect}
534+
defaultCollapseIcon={<ExpandMoreIcon />}
535+
defaultExpandIcon={<ChevronRightIcon />}
536+
sx={{
537+
flexGrow: 1,
538+
overflowY: "auto",
539+
pb: 2,
540+
"user-select": "none",
541+
}}
542+
>
543+
{renderTreeContent(filteredOptions)}
544+
</TreeView>
545+
)}
546+
</Popover>
455547
</div>
456548
);
457549
}

0 commit comments

Comments
 (0)