Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for sections in searchable filters #3778

Merged
merged 2 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions web-common/src/components/menu/core/MenuItemData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface MenuItemGroupData {
name: string;
showDivider: boolean;
items: MenuItemData[];
}

export interface MenuItemData {
name: string;
label: string;
selected: boolean;
index: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,16 @@ props as needed.
</div>
</Tooltip>
<SearchableFilterDropdown
let:toggleFloatingElement
on:apply
on:click-outside={toggleFloatingElement}
on:deselect-all
on:escape={toggleFloatingElement}
on:item-clicked
on:search
on:select-all
{selectableItems}
{selectedItems}
selectableGroups={[{ name: "", items: selectableItems }]}
selectedItems={[selectedItems]}
slot="floating-element"
let:toggleFloatingElement
/>
</WithTogglableFloatingElement>
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ props as needed.
<WithTogglableFloatingElement
alignment="start"
distance={8}
let:toggleFloatingElement
let:active
let:toggleFloatingElement
>
<Tooltip
activeDelay={60}
Expand All @@ -70,14 +70,14 @@ props as needed.
>
<!-- TODO: Switch to Measure colors once Theming supports it -->
<Chip
{...defaultChipColors}
{active}
extraRounded={false}
{label}
outline={true}
{active}
{...defaultChipColors}
on:click={toggleFloatingElement}
outline={true}
>
<div slot="body" class="flex gap-x-2">
<div class="flex gap-x-2" slot="body">
<div
class="font-bold text-ellipsis overflow-hidden whitespace-nowrap ml-2"
>
Expand All @@ -100,6 +100,8 @@ props as needed.
</div>
</Tooltip>
<SearchableFilterDropdown
allowMultiSelect={false}
let:toggleFloatingElement
on:apply
on:click-outside={toggleFloatingElement}
on:deselect-all
Expand All @@ -110,10 +112,8 @@ props as needed.
}}
on:search
on:select-all
{selectableItems}
{selectedItems}
allowMultiSelect={false}
selectableGroups={[{ name: "", items: selectableItems }]}
selectedItems={[selectedItems]}
slot="floating-element"
let:toggleFloatingElement
/>
</WithTogglableFloatingElement>
Original file line number Diff line number Diff line change
@@ -1,63 +1,55 @@
<script lang="ts">
import type { SearchableFilterSelectableItem } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";
import Spacer from "@rilldata/web-common/components/icons/Spacer.svelte";
import Divider from "@rilldata/web-common/components/menu/core/Divider.svelte";
import { getMenuGroups } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";
import type { SearchableFilterSelectableGroup } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";
import Check from "../icons/Check.svelte";
import { Menu, MenuItem } from "../menu";
import { Search } from "../search";
import Footer from "./Footer.svelte";
import Button from "../button/Button.svelte";
import { createEventDispatcher } from "svelte";
import { matchSorter } from "match-sorter";

const dispatch = createEventDispatcher();

export let selectableItems: SearchableFilterSelectableItem[];
export let selectedItems: boolean[];
export let selectableGroups: SearchableFilterSelectableGroup[];
export let selectedItems: boolean[][];
export let allowMultiSelect = true;

interface MenuItemData {
name: string;
label: string;
selected: boolean;
index: number;
}

export const setItemsVisibleBySearchString = (
items: SearchableFilterSelectableItem[],
selected: boolean[],
searchText: string
): MenuItemData[] => {
let menuEntries = items.map((item, i) => ({
name: item.name,
label: item.label,
selected: selected[i],
index: i,
}));
// if there is no search text, return menuEntries right away,
// otherwise matchSorter sorts the mentu entries
if (!searchText) return menuEntries;
return matchSorter(menuEntries, searchText, { keys: ["label"] });
};
export let showSelection = true;

let searchText = "";

$: menuItems = setItemsVisibleBySearchString(
selectableItems,
selectedItems,
searchText
);
$: menuGroups = getMenuGroups(selectableGroups, selectedItems, searchText);

$: numSelected = selectedItems?.filter((s) => s)?.length || 0;
$: numSelected = selectedItems.reduce(
(sel, items) => sel + items.filter((i) => i).length,
0
);

$: singleSelection = numSelected === 1;

$: numSelectedNotShown =
numSelected - (menuItems?.filter((item) => item.selected)?.length || 0);
numSelected -
menuGroups.reduce(
(sel, mg) => sel + mg.items.filter((i) => i.selected).length,
0
);

$: selectableCount = selectableGroups.reduce(
(sel, g) => sel + g.items.length,
0
);
$: searchResultCount = menuGroups.reduce(
(sel, mg) => sel + mg.items.length,
0
);

$: allToggleText =
numSelected === selectableItems?.length ? "Deselect all" : "Select all";
numSelected === selectableCount ? "Deselect all" : "Select all";

$: allToggleEvt =
numSelected === selectableItems?.length ? "deselect-all" : "select-all";
numSelected === selectableCount ? "deselect-all" : "select-all";

$: dispatchAllToggleEvt = () => {
dispatch(allToggleEvt);
};
Expand All @@ -80,43 +72,59 @@
</div>
<!-- apply a wrapped flex element to ensure proper bottom spacing between body and footer -->
<div class="flex flex-col flex-1 overflow-auto w-full pb-1">
{#each menuItems as { name, label, selected, index }}
<MenuItem
icon
animateSelect={false}
focusOnMount={false}
on:hover={() => {
dispatch("hover", { index, name });
}}
on:focus={() => {
dispatch("focus", { index, name });
}}
on:select={() => {
if (singleSelection && selected) return;
dispatch("item-clicked", { index, name });
}}
>
<svelte:fragment slot="icon">
{#if selected}
<Check
size="20px"
color={allowMultiSelect && singleSelection
? "#9CA3AF"
: "#15141A"}
/>
{#if searchResultCount > 0}
{#each menuGroups as { name, items, showDivider }}
{#if items.length}
{#if showDivider}
<Divider />
{/if}
</svelte:fragment>
<span class:ui-copy-disabled={!selected && allowMultiSelect}>
{#if label.length > 240}
{label.slice(0, 240)}...
{:else}
{label}
{#if name}
<span class="gap-x-3 px-3 pb-1 text-gray-500 font-semibold"
>{name}</span
>
{/if}
</span>
</MenuItem>
{#each items as { name, label, selected, index }}
<MenuItem
icon={showSelection}
animateSelect={false}
focusOnMount={false}
on:hover={() => {
dispatch("hover", { index, name });
}}
on:focus={() => {
dispatch("focus", { index, name });
}}
on:select={() => {
if (singleSelection && selected) return;
dispatch("item-clicked", { index, name });
}}
>
<svelte:fragment slot="icon">
{#if selected}
<Check
size="20px"
color={allowMultiSelect && singleSelection
? "#9CA3AF"
: "#15141A"}
/>
{:else}
<Spacer size="20px" />
{/if}
</svelte:fragment>
<span class:ui-copy-disabled={!selected && allowMultiSelect}>
{#if label.length > 240}
{label.slice(0, 240)}...
{:else}
{label}
{/if}
</span>
</MenuItem>
{/each}
{/if}
{/each}
{:else}
<div class="mt-5 ui-copy-disabled text-center">no results</div>
{/each}
{/if}
</div>
{#if allowMultiSelect}
<Footer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,48 @@
import type {
MenuItemData,
MenuItemGroupData,
} from "@rilldata/web-common/components/menu/core/MenuItemData";
import { matchSorter } from "match-sorter";

export interface SearchableFilterSelectableGroup {
name: string;
items: SearchableFilterSelectableItem[];
}

export interface SearchableFilterSelectableItem {
name: string;
label: string;
}

export function getMenuGroups(
groups: SearchableFilterSelectableGroup[],
selected: boolean[][],
searchText: string
) {
return groups.map(
(g, i) =>
<MenuItemGroupData>{
name: g.name,
showDivider: i > 0,
items: getMenuItems(g.items, selected[i] ?? [], searchText),
}
);
}

function getMenuItems(
items: SearchableFilterSelectableItem[],
selected: boolean[],
searchText: string
) {
const menuItems = items.map(
(item, i) =>
<MenuItemData>{
name: item.name,
label: item.label,
selected: selected[i],
index: i,
}
);
if (!searchText) return menuItems;
return matchSorter(menuItems, searchText, { keys: ["label"] });
}
44 changes: 25 additions & 19 deletions web-common/src/features/dashboards/filters/FilterButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
</script>

<script lang="ts">
import type { SearchableFilterSelectableGroup } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";

const {
selectors: {
dimensions: { allDimensions },
Expand All @@ -24,23 +26,27 @@
return selected?.find((f) => f.name === name) !== undefined;
}

$: selectableItems =
$allDimensions
?.map((d) => ({
name: d.name as string,
label: d.label as string,
}))
.filter((d) => {
const exclude = $isFilterExcludeMode(d.name);
return !filterExists(d.name, exclude);
}) ?? [];
$: selectableGroups = [
<SearchableFilterSelectableGroup>{
items:
$allDimensions
?.map((d) => ({
name: d.name as string,
label: d.label as string,
}))
.filter((d) => {
const exclude = $isFilterExcludeMode(d.name);
return !filterExists(d.name, exclude);
}) ?? [],
},
];
</script>

<WithTogglableFloatingElement
distance={8}
alignment="start"
let:toggleFloatingElement
distance={8}
let:active
let:toggleFloatingElement
>
<Tooltip distance={8} suppress={active}>
<button class:active on:click={toggleFloatingElement}>
Expand All @@ -50,19 +56,19 @@
</Tooltip>

<SearchableFilterDropdown
let:toggleFloatingElement
slot="floating-element"
selectedItems={[]}
allowMultiSelect={false}
{selectableItems}
on:hover
on:focus
on:escape={toggleFloatingElement}
let:toggleFloatingElement
on:click-outside={toggleFloatingElement}
on:escape={toggleFloatingElement}
on:focus
on:hover
on:item-clicked={(e) => {
toggleFloatingElement();
$potentialFilterName = e.detail.name;
}}
{selectableGroups}
selectedItems={[]}
slot="floating-element"
/>
</WithTogglableFloatingElement>

Expand Down
Loading