-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
17 changed files
with
648 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
<script setup lang="ts"> | ||
import type { ColumnFilter, Table } from "@tanstack/vue-table"; | ||
import { Filter, X } from "lucide-vue-next"; | ||
const props = defineProps<{ | ||
table: Table<never>; | ||
}>(); | ||
const activeFilterColumns = computed(() => | ||
props.table | ||
.getState() | ||
.columnFilters.filter((c: ColumnFilter) => (c.value as Array<string>).length > 0), | ||
); | ||
function removeFilters(colId: string) { | ||
props.table.getColumn(colId)?.setFilterValue([]); | ||
} | ||
function removeAllFilters() { | ||
props.table.resetColumnFilters(); | ||
} | ||
function removeValFromColumnFilter(col: ColumnFilter, val: string) { | ||
const newFilter = (col.value as Array<string>).toSpliced( | ||
(col.value as Array<string>).indexOf(val), | ||
1, | ||
); | ||
props.table.getColumn(col.id)?.setFilterValue(newFilter); | ||
} | ||
</script> | ||
|
||
<template> | ||
<DropdownMenu> | ||
<DropdownMenuTrigger as-child> | ||
<Button | ||
class="ml-auto hidden h-8 lg:flex" | ||
:disabled="activeFilterColumns.length === 0" | ||
size="sm" | ||
variant="outline" | ||
> | ||
<Filter class="mr-2 size-4" /> | ||
Active Filters | ||
<Badge class="ml-2" variant="outline">{{ activeFilterColumns.length }}</Badge> | ||
</Button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent align="end" class="max-h-[350px] w-72 max-w-none overflow-y-auto"> | ||
<DropdownMenuLabel class="flex items-center justify-between" | ||
><span>Remove filters</span | ||
><Button | ||
class="ml-2 flex h-8 gap-1" | ||
:disabled="activeFilterColumns.length === 0" | ||
size="sm" | ||
variant="outline" | ||
@click="removeAllFilters()" | ||
> | ||
<X class="size-4 align-middle hover:scale-125"></X><span>Remove all</span></Button | ||
></DropdownMenuLabel | ||
> | ||
<DropdownMenuSeparator /> | ||
<div | ||
v-for="col in activeFilterColumns" | ||
:key="col.id" | ||
class="flex justify-between p-2 text-sm" | ||
> | ||
<span | ||
>{{ props.table.getColumn(col.id)?.columnDef.header }} | ||
<Badge | ||
v-for="val in col.value" | ||
:key="val" | ||
class="mx-0.5 cursor-pointer" | ||
title="Remove" | ||
variant="secondary" | ||
@click="removeValFromColumnFilter(col, val)" | ||
>{{ val }}</Badge | ||
> </span | ||
><button @click="removeFilters(col.id)"> | ||
<X class="size-4 hover:scale-125"></X><span class="sr-only">Remove filter</span> | ||
</button> | ||
</div> | ||
<span v-if="activeFilterColumns.length === 0" class="m-2 text-sm text-neutral-600" | ||
>No filters active.</span | ||
> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<script setup lang="ts"> | ||
import type { Column } from "@tanstack/vue-table"; | ||
import { Filter } from "lucide-vue-next"; | ||
const props = defineProps<{ | ||
column: Column<never, unknown>; | ||
}>(); | ||
const facets = computed(() => | ||
[...props.column.getFacetedUniqueValues()]?.sort((a, b) => b[1] - a[1]), | ||
); | ||
const selectedValues = computed(() => new Set(props.column?.getFilterValue() as Array<string>)); | ||
</script> | ||
|
||
<template> | ||
<DropdownMenu> | ||
<DropdownMenuTrigger class="group p-1 align-middle" | ||
><Filter | ||
class="size-4 group-hover:scale-125" | ||
:class="selectedValues.size > 0 && 'fill-white'" | ||
></Filter | ||
></DropdownMenuTrigger> | ||
<DropdownMenuContent> | ||
<DropdownMenuCheckboxItem | ||
v-for="facet in facets" | ||
:key="facet[0]" | ||
:checked="selectedValues.has(facet[0])" | ||
@update:checked=" | ||
(checked) => { | ||
if (!checked) { | ||
selectedValues.delete(facet[0]); | ||
} else { | ||
selectedValues.add(facet[0]); | ||
} | ||
column?.setFilterValue([...selectedValues]); | ||
} | ||
" | ||
> | ||
{{ facet[0] }} | ||
<Badge class="ml-2" variant="outline">{{ facet[1] }}</Badge> | ||
</DropdownMenuCheckboxItem> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<script setup lang="ts"> | ||
import type { Column, Table } from "@tanstack/vue-table"; | ||
import { ChevronDown, LayoutList, ListChecks, ListTodo, TableProperties } from "lucide-vue-next"; | ||
import type { Component } from "vue"; | ||
const props = defineProps<{ | ||
table: Table<never>; | ||
}>(); | ||
const columns = computed(() => props.table.getAllColumns().filter((column) => column.getCanHide())); | ||
// Source: https://stackoverflow.com/a/64489760 | ||
function titleCase(s: string) { | ||
return s | ||
.replace(/^[-_]*(.)/, (_, c) => c.toUpperCase()) // Initial char (after -/_) | ||
.replace(/[-_]+(.)/g, (_, c) => " " + c.toUpperCase()); // First char after each -/_ | ||
} | ||
type visibilityState = "ALL_VISIBLE" | "SOME_VISIBLE" | "NONE_VISIBLE"; | ||
function getVisibilityState(category: Column<never>) { | ||
const currentState: visibilityState = category.columns.every((c) => c.getIsVisible()) | ||
? "ALL_VISIBLE" | ||
: category.columns.some((c) => c.getIsVisible()) | ||
? "SOME_VISIBLE" | ||
: "NONE_VISIBLE"; | ||
return currentState; | ||
} | ||
function toggleCategory(category: Column<never>) { | ||
let targetVisibility = true; | ||
switch (getVisibilityState(category)) { | ||
case "ALL_VISIBLE": | ||
targetVisibility = false; | ||
break; | ||
default: | ||
targetVisibility = true; | ||
} | ||
category.columns.forEach((c) => { | ||
if (c.getIsVisible() !== targetVisibility) { | ||
c.setFilterValue([]); | ||
c.toggleVisibility(targetVisibility); | ||
} | ||
}); | ||
} | ||
function getAllColumnsVisibilityState() { | ||
if (props.table.getIsAllColumnsVisible()) return "ALL_VISIBLE"; | ||
const visibleColumns = props.table.getVisibleLeafColumns(); | ||
const hidableColumns = props.table.getAllLeafColumns().filter((c) => !c.getCanHide()); | ||
return visibleColumns.length > hidableColumns.length ? "SOME_VISIBLE" : "NONE_VISIBLE"; | ||
} | ||
function toggleAllCategories() { | ||
let targetVisibility = true; | ||
switch (getAllColumnsVisibilityState()) { | ||
case "ALL_VISIBLE": | ||
targetVisibility = false; | ||
break; | ||
default: | ||
targetVisibility = true; | ||
} | ||
if (!targetVisibility) props.table.resetColumnFilters(true); | ||
props.table.toggleAllColumnsVisible(targetVisibility); | ||
} | ||
const isCollapsibleOpen = ref(columns.value.map(() => false)); | ||
const visibilityToIcon: Record<visibilityState, Component> = { | ||
ALL_VISIBLE: ListChecks, | ||
SOME_VISIBLE: ListTodo, | ||
NONE_VISIBLE: LayoutList, | ||
}; | ||
</script> | ||
|
||
<template> | ||
<DropdownMenu> | ||
<DropdownMenuTrigger as-child> | ||
<Button class="ml-auto hidden h-8 lg:flex" size="sm" variant="outline"> | ||
<TableProperties class="mr-2 size-4" /> | ||
Features | ||
</Button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent align="end" class="max-h-[350px] w-72 max-w-none overflow-y-auto"> | ||
<DropdownMenuLabel class="flex items-center justify-between" | ||
><span>Select features</span | ||
><Button class="ml-2 h-8" size="sm" variant="outline" @click.stop="toggleAllCategories()" | ||
><component | ||
:is="visibilityToIcon[getAllColumnsVisibilityState()]" | ||
class="mr-2 size-4 align-middle" | ||
></component | ||
><span class="align-middle">{{ | ||
getAllColumnsVisibilityState() === "ALL_VISIBLE" ? "Deselect all" : "Select all" | ||
}}</span></Button | ||
></DropdownMenuLabel | ||
> | ||
<DropdownMenuSeparator /> | ||
<div v-for="(group, idx) in columns" :key="group.id"> | ||
<Collapsible v-slot="{ open }" v-model:open="isCollapsibleOpen[idx]"> | ||
<CollapsibleTrigger class="flex w-full items-center gap-1 p-2 text-sm"> | ||
<Button class="mr-2 p-2" variant="outline" @click.stop="toggleCategory(group)" | ||
><component | ||
:is="visibilityToIcon[getVisibilityState(group)]" | ||
class="size-4" | ||
></component | ||
></Button> | ||
<span>{{ titleCase(group.id) }}</span> | ||
<ChevronDown class="size-4" :class="open ? 'rotate-180' : ''"></ChevronDown> | ||
</CollapsibleTrigger> | ||
|
||
<CollapsibleContent class=""> | ||
<DropdownMenuCheckboxItem | ||
v-for="column in group.columns" | ||
:key="column.id" | ||
:checked="column.getIsVisible()" | ||
@select.prevent | ||
@update:checked=" | ||
(value) => { | ||
column.toggleVisibility(!!value); | ||
column.setFilterValue([]); | ||
} | ||
" | ||
> | ||
{{ column.columnDef.header }} | ||
</DropdownMenuCheckboxItem> | ||
</CollapsibleContent> | ||
<DropdownMenuSeparator /> | ||
</Collapsible> | ||
</div> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.