Skip to content

Commit

Permalink
Merge branch 'feat/tanstack-table' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Mocca101 committed Feb 26, 2024
2 parents a350feb + 478e237 commit 61f8dfe
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 62 deletions.
29 changes: 26 additions & 3 deletions components/data-view.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts" setup>
import type { SortingState } from "@tanstack/vue-table";
import { z } from "zod";
import type { SearchFormData } from "@/components/search-form.vue";
Expand All @@ -11,8 +12,7 @@ import {
PaginationListItem,
PaginationNext,
} from "@/components/ui/pagination";
import { categories } from "@/composables/use-get-search-results";
import { project } from "@/config/project.config";
import { categories, columns, isColumn } from "@/composables/use-get-search-results";
const router = useRouter();
const route = useRoute();
Expand All @@ -21,6 +21,8 @@ const t = useTranslations();
const searchFiltersSchema = z.object({
category: z.enum(categories).catch("entityName"),
search: z.string().catch(""),
column: z.enum(columns).catch("name"),
sort: z.enum(["asc", "desc"]).catch("asc"),
page: z.coerce.number().int().positive().catch(1),
limit: z.coerce.number().int().positive().max(100).catch(20),
});
Expand All @@ -29,6 +31,12 @@ const searchFilters = computed(() => {
return searchFiltersSchema.parse(route.query);
});
const sortingState = computed(() => {
return [
{ id: searchFilters.value.column, desc: searchFilters.value.sort === "desc" },
] as SortingState;
});
type SearchFilters = z.infer<typeof searchFiltersSchema>;
function setSearchFilters(query: Partial<SearchFilters>) {
Expand All @@ -44,6 +52,18 @@ function onUpdatePage(page: number) {
setSearchFilters({ ...searchFilters.value, page });
}
function onUpdateSorting(sorting: SortingState) {
let column = searchFilters.value.column;
if (isColumn(sorting[0]?.id)) column = sorting[0].id;
setSearchFilters({
...searchFilters.value,
column,
sort: sorting[0]?.desc ? "desc" : "asc",
});
}
const { data, error, isPending, isPlaceholderData, suspense } = useGetSearchResults(
computed(() => {
const { search, category, ...params } = searchFilters.value;
Expand Down Expand Up @@ -86,7 +106,10 @@ const entities = computed(() => {
:class="{ 'opacity-50 grayscale': isLoading }"
>
<div v-if="useGetSearchResults.length > 0" class="grid gap-8">
<SearchResultsTable :entities="entities" />
<SearchResultsTable
:entities="entities"
:sorting="sortingState"
@update:sorting="onUpdateSorting" />

<Pagination
v-if="data?.pagination != null"
Expand Down
237 changes: 178 additions & 59 deletions components/search-results-table.vue
Original file line number Diff line number Diff line change
@@ -1,81 +1,200 @@
<script lang="ts" setup>
import {
type CellContext,
type Column,
type ColumnSort,
createColumnHelper,
FlexRender,
getCoreRowModel,
getSortedRowModel,
type SortingState,
useVueTable
} from '@tanstack/vue-table'
import { ArrowUpDown } from 'lucide-vue-next';
import NavLink from "@/components/nav-link.vue";
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { EntityFeature } from "@/composables/use-create-entity";
import { isColumn } from '@/composables/use-get-search-results';
const emit = defineEmits({
"update:sorting"(sorting: SortingState) {
if (!sorting.length) return false;
const containsInvalidColumn = sorting.some((sort: ColumnSort) => { // Returns true if any column is invalid
if (!isColumn(sort.id)) return true;
return false;
});
if (containsInvalidColumn) return false;
return true;
}
})
const props = defineProps<{
entities: Array<EntityFeature>;
sorting: SortingState;
}>();
const t = useTranslations();
const { d } = useI18n();
const columnHelper = createColumnHelper<EntityFeature>();
/**
* Converts a date cell value to a formatted date string.
* @param {CellContext<EntityFeature, string>} info - The cell context containing the date value.
* @returns {string} - The formatted date string.
*/
function dateCellToDateString(info: CellContext<EntityFeature, string>): string {
const date: string | null | undefined = info.getValue();
if (!date || date.includes('null')) return '';
return d(date);
}
/**
* Creates a sortable header button for the search results table.
*
* @param {Column<EntityFeature, string>} column - The column object representing the feature of an entity.
* @param {string} title - The title of the sortable header button.
* @returns {VNode} - The Vue render function for the sortable header button.
*/
function sortableHeader(column: Column<EntityFeature, string>, title: string) {
return h(Button, {
variant: 'ghost',
onClick: () => {
const currentSorting = column.getIsSorted();
emit("update:sorting", [{ id: column.id, desc: currentSorting === 'asc' ? true : false }])
},
}, () => { return [title, h(ArrowUpDown, { class: 'size-4' })] })
}
/**
* collumn.id: The ID of the column, should be the key used for sorting in the API.
* Needs to be explicitly given if the column needs to be sortable.
* @see columns
*/
const cols = [
columnHelper.accessor(
'systemClass',
{
id: 'system_class',
header: ({ column }) => { return sortableHeader(column, t("SearchResultsTable.header.class")) },
cell: info => {
const icon = getEntityIcon(info.getValue())
const tooltipWrapper = h(TooltipProvider, {}, [
h(Tooltip, {}, [
h(TooltipTrigger,
{ class: "cursor-default" },
icon ? h(icon, { class: "size-4 shrink-0" }) : h('span', {}, info.getValue())),
h(TooltipContent,
{},
t(`SystemClassNames.${info.getValue()}`))
])
])
const root = h('span', {}, [
tooltipWrapper,
h('span', { class: "sr-only" }, t(`SystemClassNames.${info.getValue()}`))
])
return root;
}
}
),
columnHelper.accessor(
'properties.title',
{
id: 'name',
header: ({ column }) => { return sortableHeader(column, t("SearchResultsTable.header.name")) },
cell: info => {
const title = info.getValue();
return h(NavLink,
{
class: "underline decoration-dotted transition hover:no-underline focus-visible:no-underline",
href: { path: `/entities/${encodeURIComponent(info.row.original.properties._id)}` }
},
title
)
}
}
),
columnHelper.accessor(
'descriptions',
{
header: t("SearchResultsTable.header.description"),
cell: info => {
const descriptions = info.getValue()
if (!Array.isArray(descriptions)) return ''
return descriptions
.filter(desc => { return desc.value })
.map((description, index) => {
if (description.value != null) {
return h('span', { key: index }, description.value)
}
return
});
}
}
),
columnHelper.accessor(
row => { return `${row.when?.timespans?.[0]?.start?.earliest} ` },
{
id: 'begin_from',
header: ({ column }) => { return sortableHeader(column, t("SearchResultsTable.header.begin")) },
cell: (info) => { return dateCellToDateString(info) }
}
),
columnHelper.accessor(
row => { return `${row.when?.timespans?.[0]?.end?.earliest} ` },
{
id: 'end_from',
header: ({ column }) => { return sortableHeader(column, t("SearchResultsTable.header.end")) },
cell: (info) => { return dateCellToDateString(info) }
}
)
]
const table = useVueTable({
get data() { return props.entities },
get columns() { return cols },
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
state: {
get sorting() { return props.sorting },
}
})
</script>

<template>
<Table>
<TableCaption class="sr-only">{{ t("SearchResultsTable.search-results") }}</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-14">
{{ t("SearchResultsTable.header.class") }}
</TableHead>
<TableHead>
{{ t("SearchResultsTable.header.name") }}
</TableHead>
<TableHead>
{{ t("SearchResultsTable.header.description") }}
</TableHead>
<TableHead class="text-right">
{{ t("SearchResultsTable.header.begin") }}
</TableHead>
<TableHead class="text-right">
{{ t("SearchResultsTable.header.end") }}
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="entity of props.entities" :key="entity.properties._id">
<TableCell class="font-medium">
<TooltipProvider>
<Tooltip>
<TooltipTrigger class="cursor-default">
<Component :is="getEntityIcon(entity.systemClass)" class="size-4 shrink-0" />
</TooltipTrigger>
<TooltipContent>
{{ t(`SystemClassNames.${entity.systemClass}`) }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span class="sr-only">{{ t(`SystemClassNames.${entity.systemClass}`) }}</span>
</TableCell>
<TableCell>
<NavLink
class="underline decoration-dotted transition hover:no-underline focus-visible:no-underline"
:href="{ path: `/entities/${encodeURIComponent(entity.properties._id)}` }"
>
{{ entity.properties.title }}
</NavLink>
</TableCell>
<TableCell>
<template v-for="(description, index) of entity.descriptions" :key="index">
<span v-if="description.value != null">{{ description.value }}</span>
</template>
</TableCell>
<TableCell class="text-right">
<template v-for="(timespan, index) of entity.when?.timespans" :key="index">
<!-- FIXME: why earliest -->
<span v-if="timespan.start?.earliest != null">
{{ d(timespan.start.earliest) }}
</span>
</template>
</TableCell>
<TableCell class="text-right">
<template v-for="(timespan, index) of entity.when?.timespans" :key="index">
<!-- FIXME: why earliest -->
<span v-if="timespan.end?.earliest != null">
{{ d(timespan.end.earliest) }}
</span>
</template>
</TableCell>
</TableRow>
<template v-if="table.getRowModel().rows?.length">
<TableRow
v-for="row in table.getRowModel().rows"
:key="row.id"
:data-state="row.getIsSelected() ? 'selected' : undefined">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</template>
23 changes: 23 additions & 0 deletions composables/use-get-search-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@ export const categories = [
"valueTypeID",
] as const;

/**
* The columns that can be sorted on.
* @id apiColumns
*/
export const columns = [
"id",
"name",
"cidoc_class",
"system_class",
"begin_from",
"begin_to",
"end_from",
"end_to",
] as const;

export type Column = (typeof columns)[number];

// Write a check to see if an object is of typed column
export function isColumn(value: unknown): value is Column {
return columns.includes(value as Column);
}


export type Category = (typeof categories)[number];

export const operators = [
Expand Down
1 change: 1 addition & 0 deletions messages/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"header": {
"begin": "Anfang",
"class": "Klasse",
"dates": "Datum",
"description": "Beschreibung",
"end": "Ende",
"name": "Name"
Expand Down
1 change: 1 addition & 0 deletions messages/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"header": {
"begin": "Begin",
"class": "Class",
"dates": "Dates",
"description": "Description",
"end": "End",
"name": "Name"
Expand Down
8 changes: 8 additions & 0 deletions utils/data-table-value-updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Updater } from "@tanstack/vue-table";
import type { Ref } from "vue";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
ref.value = typeof updaterOrValue === "function" ? updaterOrValue(ref.value) : updaterOrValue;
}

0 comments on commit 61f8dfe

Please sign in to comment.