Repository: kianbazza/ui
Pull request #28: feat(data-table-filter)!: deep-keyed properties and accessor functions in `defineMeta` helper
Author: login: Drosscend
Labels: name: Data table filter
description: Type > Data table filter
name: Feature
description: Type > Feature
Created: 2025-04-01T09:48:11Z | Merged: 2025-04-02T03:04:27Z

## Feature Overview
We've enhanced the data table filtering library to support nested property paths in column definitions. This feature allows you to directly reference and filter on nested properties within complex data structures using dot notation (e.g., `user.name` instead of just `name`).

## Problem Solved
Previously, the library only supported filtering on direct properties of data objects. When working with complex data structures that include nested objects (like a feedback entry with a user object), you couldn't directly reference nested properties in your column definitions. This limitation required workarounds such as:

1. Flattening data structures before displaying them
2. Creating accessor functions to extract nested values
3. Implementing custom filter functions for nested properties

## Benefits

### 1. Improved Type Safety
The implementation leverages TypeScript's type system to provide full type safety for nested paths. The `NestedPaths<T>` type utility ensures that only valid paths within your data structure can be used.

### 2. Enhanced Developer Experience
- **Autocompletion**: TypeScript will suggest valid nested paths as you type
- **Error Prevention**: Invalid paths are caught at compile time, not runtime
- **Cleaner Code**: No need for complex accessor functions or data transformation

### 3. Consistent API
The API remains unchanged - you still use the same `defineMeta` function, but now it accepts nested paths.

## Usage Example

```tsx
// Before - Only direct properties supported
meta: defineMeta("username", {
  displayName: "User Name",
  type: "text",
  icon: UserIcon,
})

// After - Nested properties supported
meta: defineMeta("user.name", {
  displayName: "User Name",
  type: "text",
  icon: UserIcon,
})
```

## Implementation Details
The feature is implemented through a new type utility `NestedPaths<T>` that generates all possible paths for a type, including both direct properties and one level of nested paths. The `defineMeta` function now uses this type to constrain the path parameter.

## Limitations
Currently, the implementation supports only one level of nesting (e.g., `user.name` but not `user.address.city`). Future enhancements could extend this to support deeper nesting levels if needed.

## Conclusion
This enhancement makes the data table filtering library more flexible and powerful, especially when working with complex data structures. It reduces the need for data transformation and simplifies column definitions while maintaining type safety.

---
# Merge Checklist

- [x] Update docs
- [x] Update demo
- [x] Build registry
- [x] Update examples (vite-basic)
- [x] Format/lint



Diff:
diff --git a/apps/web/components/data-table-filter/demo.tsx b/apps/web/components/data-table-filter/demo.tsx
index 7cb2ff2a..dd724182 100644
--- a/apps/web/components/data-table-filter/demo.tsx
+++ b/apps/web/components/data-table-filter/demo.tsx
@@ -190,7 +190,7 @@ export const columns = [
       )
     },
     filterFn: filterFn('option'),
-    meta: defineMeta('assignee', {
+    meta: defineMeta((row) => row.assignee, {
       displayName: 'Assignee',
       type: 'option',
       icon: UserCheckIcon,
@@ -212,6 +212,20 @@ export const columns = [
       }),
     }),
   }),
+  columnHelper.accessor((row) => row.assignee?.name, {
+    id: 'assigneeName',
+    header: 'Assignee (Name)',
+    filterFn: filterFn('option'),
+    meta: defineMeta((row) => row.assignee?.name, {
+      displayName: 'Assignee name',
+      type: 'option',
+      icon: UserCheckIcon,
+      transformOptionFn: (name) => ({
+        value: name,
+        label: name,
+      }),
+    }),
+  }),
   columnHelper.accessor('labelIds', {
     id: 'labels',
     header: 'Labels',
@@ -237,7 +251,7 @@ export const columns = [
         </div>
       )
     },
-    meta: defineMeta('labelIds', {
+    meta: defineMeta((row) => row.labelIds, {
       displayName: 'Labels',
       type: 'multiOption',
       icon: TagIcon,
diff --git a/apps/web/content/docs/data-table-filter.mdx b/apps/web/content/docs/data-table-filter.mdx
index fcf77432..a4dadaab 100644
--- a/apps/web/content/docs/data-table-filter.mdx
+++ b/apps/web/content/docs/data-table-filter.mdx
@@ -203,11 +203,13 @@ For the filter function, we provide one for you - it is conveniently called `fil
 
 For the column meta, we provide a helper function called `defineMeta()` which takes two arguments: **(1)** the property name from your data object, and **(2)** an object containing the column meta.
 
+When specifying the property name, you must use an accessor function, as demonstrated below.
+
 ```ts
 export const columns = [
   columnHelper.accessor('status', {
     filterFn: filterFn('option'),
-    meta: defineMeta('status', {
+    meta: defineMeta(row => row.status, {
       displayName: 'Status',
       type: 'option',
       icon: CircleDotDashedIcon,
@@ -363,3 +365,56 @@ We can break down the `PropertyFilterValueController` as an example:
 />
 
 The `PropertyFilterOperatorController` has a similar composition and can be inferred from the above description and image.
+
+## Changelog
+
+### 2025.04.01
+
+> [!DANGER] This is a breaking change.
+
+This adds support for filtering columns where the column value is not strictly a property of the original data. This was not possible before, due to the limitation of `defineMeta`'s first argument, which only accepted a direct property on the initial data type.
+
+You can now filter columns where the value is:
+
+- a deeply nested property (i.e. `user.name`)
+- accessed using a function (i.e. `row => row.user.name.split(' ')[0]`)
+
+To accomplish this, we've decided to change the interface for the `defineMeta` helper function. The first property is now an **accessor function**, instead of an accessor key.
+
+See the example below for how to migrate:
+
+```ts
+type Issue = {
+  status: string
+  user: {
+    name: string
+  }
+}
+```
+
+```ts {15-22}
+const columns = [
+  /* ... */
+  columnHelper.accessor('status', {
+    meta: defineMeta(
+      'status', // [!code --]
+      row => row.status, // [!code ++]
+      { 
+        type: 'option',
+        icon: CircleDotDashedIcon,
+        options: ISSUE_STATUSES,
+      }
+    ),
+  }),
+  columnHelper.accessor(row => row.user.name, {
+    meta: defineMeta(
+      row => row.user.name,
+      { 
+        type: 'option',
+        icon: AvatarIcon,
+        /* ... */
+      }
+    ),
+  }),
+]
+```
diff --git a/apps/web/public/r/data-table-filter.json b/apps/web/public/r/data-table-filter.json
index 106dd327..50275761 100644
--- a/apps/web/public/r/data-table-filter.json
+++ b/apps/web/public/r/data-table-filter.json
@@ -34,7 +34,7 @@
     },
     {
       "path": "registry/data-table-filter/lib/filters.ts",
-      "content": "import '@tanstack/table-core'\nimport type { Column, Row, RowData } from '@tanstack/react-table'\nimport type { ColumnMeta, Table } from '@tanstack/react-table'\nimport {\n  endOfDay,\n  isAfter,\n  isBefore,\n  isSameDay,\n  isWithinInterval,\n  startOfDay,\n} from 'date-fns'\nimport type { LucideIcon } from 'lucide-react'\nimport { intersection, uniq } from './array'\n\nexport type ElementType<T> = T extends (infer U)[] ? U : T\n\ndeclare module '@tanstack/react-table' {\n  interface ColumnMeta<TData extends RowData, TValue> {\n    /* The display name of the column. */\n    displayName: string\n\n    /* The column icon. */\n    icon: LucideIcon\n\n    /* The data type of the column. */\n    type: ColumnDataType\n\n    /* An optional list of options for the column. */\n    /* This is used for columns with type 'option' or 'multiOption'. */\n    /* If the options are known ahead of time, they can be defined here. */\n    /* Otherwise, they will be dynamically generated based on the data. */\n    options?: ColumnOption[]\n\n    /* An optional function to transform columns with type 'option' or 'multiOption'. */\n    /* This is used to convert each raw option into a ColumnOption. */\n    transformOptionFn?: (\n      value: ElementType<NonNullable<TValue>>,\n    ) => ColumnOption\n\n    /* An optional \"soft\" max for the number range slider. */\n    /* This is used for columns with type 'number'. */\n    max?: number\n  }\n}\n\nexport function defineMeta<\n  TData,\n  TKey extends keyof TData,\n  TType extends ColumnDataType,\n>(\n  key: TKey,\n  meta: Omit<ColumnMeta<TData, TData[TKey]>, 'type'> & {\n    type: TType\n  },\n): ColumnMeta<TData, TData[TKey]> {\n  return meta\n}\n\n/*\n * Represents a possible value for a column property of type 'option' or 'multiOption'.\n */\nexport interface ColumnOption {\n  /* The label to display for the option. */\n  label: string\n  /* The internal value of the option. */\n  value: string\n  /* An optional icon to display next to the label. */\n  icon?: React.ReactElement | React.ElementType\n}\n\n/*\n * Represents the data type of a column.\n */\nexport type ColumnDataType =\n  /* The column value is a string that should be searchable. */\n  | 'text'\n  | 'number'\n  | 'date'\n  /* The column value can be a single value from a list of options. */\n  | 'option'\n  /* The column value can be zero or more values from a list of options. */\n  | 'multiOption'\n\n/* Operators for text data */\nexport type TextFilterOperator = 'contains' | 'does not contain'\n\n/* Operators for number data */\nexport type NumberFilterOperator =\n  | 'is'\n  | 'is not'\n  | 'is less than'\n  | 'is greater than or equal to'\n  | 'is greater than'\n  | 'is less than or equal to'\n  | 'is between'\n  | 'is not between'\n\n/* Operators for date data */\nexport type DateFilterOperator =\n  | 'is'\n  | 'is not'\n  | 'is before'\n  | 'is on or after'\n  | 'is after'\n  | 'is on or before'\n  | 'is between'\n  | 'is not between'\n\n/* Operators for option data */\nexport type OptionFilterOperator = 'is' | 'is not' | 'is any of' | 'is none of'\n\n/* Operators for multi-option data */\nexport type MultiOptionFilterOperator =\n  | 'include'\n  | 'exclude'\n  | 'include any of'\n  | 'include all of'\n  | 'exclude if any of'\n  | 'exclude if all'\n\n/* Maps filter operators to their respective data types */\ntype FilterOperators = {\n  text: TextFilterOperator\n  number: NumberFilterOperator\n  date: DateFilterOperator\n  option: OptionFilterOperator\n  multiOption: MultiOptionFilterOperator\n}\n\n/* Maps filter values to their respective data types */\nexport type FilterTypes = {\n  text: string\n  number: number\n  date: Date\n  option: string\n  multiOption: string[]\n}\n\n/*\n *\n * FilterValue is a type that represents a filter value for a specific column.\n *\n * It consists of:\n * - Operator: The operator to be used for the filter.\n * - Values: An array of values to be used for the filter.\n *\n */\nexport type FilterValue<T extends ColumnDataType, TData> = {\n  operator: FilterOperators[T]\n  values: Array<FilterTypes[T]>\n  columnMeta: Column<TData>['columnDef']['meta']\n}\n\n/*\n * FilterDetails is a type that represents the details of all the filter operators for a specific column data type.\n */\nexport type FilterDetails<T extends ColumnDataType> = {\n  [key in FilterOperators[T]]: FilterOperatorDetails<key, T>\n}\n\ntype FilterOperatorDetailsBase<OperatorValue, T extends ColumnDataType> = {\n  /* The operator value. Usually the string representation of the operator. */\n  value: OperatorValue\n  /* The label for the operator, to show in the UI. */\n  label: string\n  /* How much data the operator applies to. */\n  target: 'single' | 'multiple'\n  /* The plural form of the operator, if applicable. */\n  singularOf?: FilterOperators[T]\n  /* The singular form of the operator, if applicable. */\n  pluralOf?: FilterOperators[T]\n  /* All related operators. Normally, all the operators which share the same target. */\n  relativeOf: FilterOperators[T] | Array<FilterOperators[T]>\n  /* Whether the operator is negated. */\n  isNegated: boolean\n  /* If the operator is not negated, this provides the negated equivalent. */\n  negation?: FilterOperators[T]\n  /* If the operator is negated, this provides the positive equivalent. */\n  negationOf?: FilterOperators[T]\n}\n\n/*\n *\n * FilterOperatorDetails is a type that provides details about a filter operator for a specific column data type.\n * It extends FilterOperatorDetailsBase with additional logic and contraints on the defined properties.\n *\n */\nexport type FilterOperatorDetails<\n  OperatorValue,\n  T extends ColumnDataType,\n> = FilterOperatorDetailsBase<OperatorValue, T> &\n  (\n    | { singularOf?: never; pluralOf?: never }\n    | { target: 'single'; singularOf: FilterOperators[T]; pluralOf?: never }\n    | { target: 'multiple'; singularOf?: never; pluralOf: FilterOperators[T] }\n  ) &\n  (\n    | { isNegated: false; negation: FilterOperators[T]; negationOf?: never }\n    | { isNegated: true; negation?: never; negationOf: FilterOperators[T] }\n  )\n\n/* Details for all the filter operators for option data type */\nexport const optionFilterDetails = {\n  is: {\n    label: 'is',\n    value: 'is',\n    target: 'single',\n    singularOf: 'is not',\n    relativeOf: 'is any of',\n    isNegated: false,\n    negation: 'is not',\n  },\n  'is not': {\n    label: 'is not',\n    value: 'is not',\n    target: 'single',\n    singularOf: 'is',\n    relativeOf: 'is none of',\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is any of': {\n    label: 'is any of',\n    value: 'is any of',\n    target: 'multiple',\n    pluralOf: 'is',\n    relativeOf: 'is',\n    isNegated: false,\n    negation: 'is none of',\n  },\n  'is none of': {\n    label: 'is none of',\n    value: 'is none of',\n    target: 'multiple',\n    pluralOf: 'is not',\n    relativeOf: 'is not',\n    isNegated: true,\n    negationOf: 'is any of',\n  },\n} as const satisfies FilterDetails<'option'>\n\n/* Details for all the filter operators for multi-option data type */\nexport const multiOptionFilterDetails = {\n  include: {\n    label: 'include',\n    value: 'include',\n    target: 'single',\n    singularOf: 'include any of',\n    relativeOf: 'exclude',\n    isNegated: false,\n    negation: 'exclude',\n  },\n  exclude: {\n    label: 'exclude',\n    value: 'exclude',\n    target: 'single',\n    singularOf: 'exclude if any of',\n    relativeOf: 'include',\n    isNegated: true,\n    negationOf: 'include',\n  },\n  'include any of': {\n    label: 'include any of',\n    value: 'include any of',\n    target: 'multiple',\n    pluralOf: 'include',\n    relativeOf: ['exclude if all', 'include all of', 'exclude if any of'],\n    isNegated: false,\n    negation: 'exclude if all',\n  },\n  'exclude if all': {\n    label: 'exclude if all',\n    value: 'exclude if all',\n    target: 'multiple',\n    pluralOf: 'exclude',\n    relativeOf: ['include any of', 'include all of', 'exclude if any of'],\n    isNegated: true,\n    negationOf: 'include any of',\n  },\n  'include all of': {\n    label: 'include all of',\n    value: 'include all of',\n    target: 'multiple',\n    pluralOf: 'include',\n    relativeOf: ['include any of', 'exclude if all', 'exclude if any of'],\n    isNegated: false,\n    negation: 'exclude if any of',\n  },\n  'exclude if any of': {\n    label: 'exclude if any of',\n    value: 'exclude if any of',\n    target: 'multiple',\n    pluralOf: 'exclude',\n    relativeOf: ['include any of', 'exclude if all', 'include all of'],\n    isNegated: true,\n    negationOf: 'include all of',\n  },\n} as const satisfies FilterDetails<'multiOption'>\n\n/* Details for all the filter operators for date data type */\nexport const dateFilterDetails = {\n  is: {\n    label: 'is',\n    value: 'is',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: 'is after',\n    isNegated: false,\n    negation: 'is before',\n  },\n  'is not': {\n    label: 'is not',\n    value: 'is not',\n    target: 'single',\n    singularOf: 'is not between',\n    relativeOf: [\n      'is',\n      'is before',\n      'is on or after',\n      'is after',\n      'is on or before',\n    ],\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is before': {\n    label: 'is before',\n    value: 'is before',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is on or after',\n      'is after',\n      'is on or before',\n    ],\n    isNegated: false,\n    negation: 'is on or after',\n  },\n  'is on or after': {\n    label: 'is on or after',\n    value: 'is on or after',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: ['is', 'is not', 'is before', 'is after', 'is on or before'],\n    isNegated: false,\n    negation: 'is before',\n  },\n  'is after': {\n    label: 'is after',\n    value: 'is after',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is before',\n      'is on or after',\n      'is on or before',\n    ],\n    isNegated: false,\n    negation: 'is on or before',\n  },\n  'is on or before': {\n    label: 'is on or before',\n    value: 'is on or before',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: ['is', 'is not', 'is after', 'is on or after', 'is before'],\n    isNegated: false,\n    negation: 'is after',\n  },\n  'is between': {\n    label: 'is between',\n    value: 'is between',\n    target: 'multiple',\n    pluralOf: 'is',\n    relativeOf: 'is not between',\n    isNegated: false,\n    negation: 'is not between',\n  },\n  'is not between': {\n    label: 'is not between',\n    value: 'is not between',\n    target: 'multiple',\n    pluralOf: 'is not',\n    relativeOf: 'is between',\n    isNegated: true,\n    negationOf: 'is between',\n  },\n} as const satisfies FilterDetails<'date'>\n\n/* Details for all the filter operators for text data type */\nexport const textFilterDetails = {\n  contains: {\n    label: 'contains',\n    value: 'contains',\n    target: 'single',\n    relativeOf: 'does not contain',\n    isNegated: false,\n    negation: 'does not contain',\n  },\n  'does not contain': {\n    label: 'does not contain',\n    value: 'does not contain',\n    target: 'single',\n    relativeOf: 'contains',\n    isNegated: true,\n    negationOf: 'contains',\n  },\n} as const satisfies FilterDetails<'text'>\n\n/* Details for all the filter operators for number data type */\nexport const numberFilterDetails = {\n  is: {\n    label: 'is',\n    value: 'is',\n    target: 'single',\n    relativeOf: [\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is not',\n  },\n  'is not': {\n    label: 'is not',\n    value: 'is not',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is greater than': {\n    label: '>',\n    value: 'is greater than',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is less than or equal to',\n  },\n  'is greater than or equal to': {\n    label: '>=',\n    value: 'is greater than or equal to',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n    ],\n    isNegated: false,\n    negation: 'is less than or equal to',\n  },\n  'is less than': {\n    label: '<',\n    value: 'is less than',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is greater than',\n  },\n  'is less than or equal to': {\n    label: '<=',\n    value: 'is less than or equal to',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is greater than or equal to',\n  },\n  'is between': {\n    label: 'is between',\n    value: 'is between',\n    target: 'multiple',\n    relativeOf: 'is not between',\n    isNegated: false,\n    negation: 'is not between',\n  },\n  'is not between': {\n    label: 'is not between',\n    value: 'is not between',\n    target: 'multiple',\n    relativeOf: 'is between',\n    isNegated: true,\n    negationOf: 'is between',\n  },\n} as const satisfies FilterDetails<'number'>\n\n/* Maps column data types to their respective filter operator details */\ntype FilterTypeOperatorDetails = {\n  [key in ColumnDataType]: FilterDetails<key>\n}\n\nexport const filterTypeOperatorDetails: FilterTypeOperatorDetails = {\n  text: textFilterDetails,\n  number: numberFilterDetails,\n  date: dateFilterDetails,\n  option: optionFilterDetails,\n  multiOption: multiOptionFilterDetails,\n}\n\n/*\n *\n * Determines the new operator for a filter based on the current operator, old and new filter values.\n *\n * This handles cases where the filter values have transitioned from a single value to multiple values (or vice versa),\n * and the current operator needs to be transitioned to its plural form (or singular form).\n *\n * For example, if the current operator is 'is', and the new filter values have a length of 2, the\n * new operator would be 'is any of'.\n *\n */\nexport function determineNewOperator<T extends ColumnDataType>(\n  type: T,\n  oldVals: Array<FilterTypes[T]>,\n  nextVals: Array<FilterTypes[T]>,\n  currentOperator: FilterOperators[T],\n): FilterOperators[T] {\n  const a =\n    Array.isArray(oldVals) && Array.isArray(oldVals[0])\n      ? oldVals[0].length\n      : oldVals.length\n  const b =\n    Array.isArray(nextVals) && Array.isArray(nextVals[0])\n      ? nextVals[0].length\n      : nextVals.length\n\n  // If filter size has not transitioned from single to multiple (or vice versa)\n  // or is unchanged, return the current operator.\n  if (a === b || (a >= 2 && b >= 2) || (a <= 1 && b <= 1))\n    return currentOperator\n\n  const opDetails = filterTypeOperatorDetails[type][currentOperator]\n\n  // Handle transition from single to multiple filter values.\n  if (a < b && b >= 2) return opDetails.singularOf ?? currentOperator\n  // Handle transition from multiple to single filter values.\n  if (a > b && b <= 1) return opDetails.pluralOf ?? currentOperator\n  return currentOperator\n}\n\n/**********************************************************************************************************\n ***** Filter Functions ******\n **********************************************************************************************************\n * These are functions that filter data based on the current filter values, column data type, and operator.\n * There exists a separate filter function for each column data type.\n *\n * Two variants of the filter functions are provided - as an example, we will take the optionFilterFn:\n * 1. optionFilterFn: takes in a row, columnId, and filterValue.\n * 2. __optionFilterFn: takes in an inputData and filterValue.\n *\n * __optionFilterFn is a private function that is used by filterFn to perform the actual filtering.\n * *********************************************************************************************************/\n\n/*\n * Returns a filter function for a given column data type.\n * This function is used to determine the appropriate filter function to use based on the column data type.\n */\nexport function filterFn(dataType: ColumnDataType) {\n  switch (dataType) {\n    case 'option':\n      return optionFilterFn\n    case 'multiOption':\n      return multiOptionFilterFn\n    case 'date':\n      return dateFilterFn\n    case 'text':\n      return textFilterFn\n    case 'number':\n      return numberFilterFn\n    default:\n      throw new Error('Invalid column data type')\n  }\n}\n\nexport function optionFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'option', TData>,\n) {\n  const value = row.getValue(columnId)\n\n  if (!value) return false\n\n  const columnMeta = filterValue.columnMeta!\n\n  if (typeof value === 'string') {\n    return __optionFilterFn(value, filterValue)\n  }\n\n  if (isColumnOption(value)) {\n    return __optionFilterFn(value.value, filterValue)\n  }\n\n  const sanitizedValue = columnMeta.transformOptionFn!(value as never)\n  return __optionFilterFn(sanitizedValue.value, filterValue)\n}\n\nexport function __optionFilterFn<TData>(\n  inputData: string,\n  filterValue: FilterValue<'option', TData>,\n) {\n  if (!inputData) return false\n  if (filterValue.values.length === 0) return true\n\n  const value = inputData.toString().toLowerCase()\n\n  const found = !!filterValue.values.find((v) => v.toLowerCase() === value)\n\n  switch (filterValue.operator) {\n    case 'is':\n    case 'is any of':\n      return found\n    case 'is not':\n    case 'is none of':\n      return !found\n  }\n}\n\nexport function isColumnOption(value: unknown): value is ColumnOption {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    'value' in value &&\n    'label' in value\n  )\n}\n\nexport function isColumnOptionArray(value: unknown): value is ColumnOption[] {\n  return Array.isArray(value) && value.every(isColumnOption)\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n  return Array.isArray(value) && value.every((v) => typeof v === 'string')\n}\n\nexport function multiOptionFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'multiOption', TData>,\n) {\n  const value = row.getValue(columnId)\n\n  if (!value) return false\n\n  const columnMeta = filterValue.columnMeta!\n\n  if (isStringArray(value)) {\n    return __multiOptionFilterFn(value, filterValue)\n  }\n\n  if (isColumnOptionArray(value)) {\n    return __multiOptionFilterFn(\n      value.map((v) => v.value),\n      filterValue,\n    )\n  }\n\n  const sanitizedValue = (value as never[]).map((v) =>\n    columnMeta.transformOptionFn!(v),\n  )\n\n  return __multiOptionFilterFn(\n    sanitizedValue.map((v) => v.value),\n    filterValue,\n  )\n}\n\nexport function __multiOptionFilterFn<TData>(\n  inputData: string[],\n  filterValue: FilterValue<'multiOption', TData>,\n) {\n  if (!inputData) return false\n\n  if (\n    filterValue.values.length === 0 ||\n    !filterValue.values[0] ||\n    filterValue.values[0].length === 0\n  )\n    return true\n\n  const values = uniq(inputData)\n  const filterValues = uniq(filterValue.values[0])\n\n  switch (filterValue.operator) {\n    case 'include':\n    case 'include any of':\n      return intersection(values, filterValues).length > 0\n    case 'exclude':\n      return intersection(values, filterValues).length === 0\n    case 'exclude if any of':\n      return !(intersection(values, filterValues).length > 0)\n    case 'include all of':\n      return intersection(values, filterValues).length === filterValues.length\n    case 'exclude if all':\n      return !(\n        intersection(values, filterValues).length === filterValues.length\n      )\n  }\n}\n\nexport function dateFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'date', TData>,\n) {\n  const valueStr = row.getValue<Date>(columnId)\n\n  return __dateFilterFn(valueStr, filterValue)\n}\n\nexport function __dateFilterFn<TData>(\n  inputData: Date,\n  filterValue: FilterValue<'date', TData>,\n) {\n  if (!filterValue || filterValue.values.length === 0) return true\n\n  if (\n    dateFilterDetails[filterValue.operator].target === 'single' &&\n    filterValue.values.length > 1\n  )\n    throw new Error('Singular operators require at most one filter value')\n\n  if (\n    filterValue.operator in ['is between', 'is not between'] &&\n    filterValue.values.length !== 2\n  )\n    throw new Error('Plural operators require two filter values')\n\n  const filterVals = filterValue.values\n  const d1 = filterVals[0]\n  const d2 = filterVals[1]\n\n  const value = inputData\n\n  switch (filterValue.operator) {\n    case 'is':\n      return isSameDay(value, d1)\n    case 'is not':\n      return !isSameDay(value, d1)\n    case 'is before':\n      return isBefore(value, startOfDay(d1))\n    case 'is on or after':\n      return isSameDay(value, d1) || isAfter(value, startOfDay(d1))\n    case 'is after':\n      return isAfter(value, startOfDay(d1))\n    case 'is on or before':\n      return isSameDay(value, d1) || isBefore(value, startOfDay(d1))\n    case 'is between':\n      return isWithinInterval(value, {\n        start: startOfDay(d1),\n        end: endOfDay(d2),\n      })\n    case 'is not between':\n      return !isWithinInterval(value, {\n        start: startOfDay(filterValue.values[0]),\n        end: endOfDay(filterValue.values[1]),\n      })\n  }\n}\n\nexport function textFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'text', TData>,\n) {\n  const value = row.getValue<string>(columnId) ?? ''\n\n  return __textFilterFn(value, filterValue)\n}\n\nexport function __textFilterFn<TData>(\n  inputData: string,\n  filterValue: FilterValue<'text', TData>,\n) {\n  if (!filterValue || filterValue.values.length === 0) return true\n\n  const value = inputData.toLowerCase().trim()\n  const filterStr = filterValue.values[0].toLowerCase().trim()\n\n  if (filterStr === '') return true\n\n  const found = value.includes(filterStr)\n\n  switch (filterValue.operator) {\n    case 'contains':\n      return found\n    case 'does not contain':\n      return !found\n  }\n}\n\nexport function numberFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'number', TData>,\n) {\n  const value = row.getValue<number>(columnId)\n\n  return __numberFilterFn(value, filterValue)\n}\n\nexport function __numberFilterFn<TData>(\n  inputData: number,\n  filterValue: FilterValue<'number', TData>,\n) {\n  if (!filterValue || !filterValue.values || filterValue.values.length === 0) {\n    return true\n  }\n\n  const value = inputData\n  const filterVal = filterValue.values[0]\n\n  switch (filterValue.operator) {\n    case 'is':\n      return value === filterVal\n    case 'is not':\n      return value !== filterVal\n    case 'is greater than':\n      return value > filterVal\n    case 'is greater than or equal to':\n      return value >= filterVal\n    case 'is less than':\n      return value < filterVal\n    case 'is less than or equal to':\n      return value <= filterVal\n    case 'is between': {\n      const lowerBound = filterValue.values[0]\n      const upperBound = filterValue.values[1]\n      return value >= lowerBound && value <= upperBound\n    }\n    case 'is not between': {\n      const lowerBound = filterValue.values[0]\n      const upperBound = filterValue.values[1]\n      return value < lowerBound || value > upperBound\n    }\n    default:\n      return true\n  }\n}\n\nexport function createNumberRange(values: number[] | undefined) {\n  let a = 0\n  let b = 0\n\n  if (!values || values.length === 0) return [a, b]\n  if (values.length === 1) {\n    a = values[0]\n  } else {\n    a = values[0]\n    b = values[1]\n  }\n\n  const [min, max] = a < b ? [a, b] : [b, a]\n\n  return [min, max]\n}\n\n/*** Table helpers ***/\n\nexport function getColumn<TData>(table: Table<TData>, id: string) {\n  const column = table.getColumn(id)\n\n  if (!column) {\n    throw new Error(`Column with id ${id} not found`)\n  }\n\n  return column\n}\n\nexport function getColumnMeta<TData>(table: Table<TData>, id: string) {\n  const column = getColumn(table, id)\n\n  if (!column.columnDef.meta) {\n    throw new Error(`Column meta not found for column ${id}`)\n  }\n\n  return column.columnDef.meta\n}\n",
+      "content": "import '@tanstack/table-core'\nimport type {\n  AccessorFn,\n  Column,\n  DeepKeys,\n  DeepValue,\n  Row,\n  RowData,\n} from '@tanstack/react-table'\nimport type { ColumnMeta, Table } from '@tanstack/react-table'\nimport {\n  endOfDay,\n  isAfter,\n  isBefore,\n  isSameDay,\n  isWithinInterval,\n  startOfDay,\n} from 'date-fns'\nimport type { LucideIcon } from 'lucide-react'\nimport { intersection, uniq } from './array'\n\nexport type ElementType<T> = T extends (infer U)[] ? U : T\n\ndeclare module '@tanstack/react-table' {\n  interface ColumnMeta<TData extends RowData, TValue> {\n    /* The display name of the column. */\n    displayName: string\n\n    /* The column icon. */\n    icon: LucideIcon\n\n    /* The data type of the column. */\n    type: ColumnDataType\n\n    /* An optional list of options for the column. */\n    /* This is used for columns with type 'option' or 'multiOption'. */\n    /* If the options are known ahead of time, they can be defined here. */\n    /* Otherwise, they will be dynamically generated based on the data. */\n    options?: ColumnOption[]\n\n    /* An optional function to transform columns with type 'option' or 'multiOption'. */\n    /* This is used to convert each raw option into a ColumnOption. */\n    transformOptionFn?: (\n      value: ElementType<NonNullable<TValue>>,\n    ) => ColumnOption\n\n    /* An optional \"soft\" max for the number range slider. */\n    /* This is used for columns with type 'number'. */\n    max?: number\n  }\n}\n\n/* TODO: Allow both accessorFn and accessorKey */\nexport function defineMeta<\n  TData,\n  /* Only accessorFn - WORKS */\n  TAccessor extends AccessorFn<TData>,\n  TVal extends ReturnType<TAccessor>,\n  /* Only accessorKey - WORKS */\n  // TAccessor extends DeepKeys<TData>,\n  // TVal extends DeepValue<TData, TAccessor>,\n\n  /* Both accessorKey and accessorFn - BROKEN */\n  /* ISSUE: Won't infer transformOptionFn input type correctly. */\n  // TAccessor extends AccessorFn<TData> | DeepKeys<TData>,\n  // TVal extends TAccessor extends AccessorFn<TData>\n  // ? ReturnType<TAccessor>\n  // : TAccessor extends DeepKeys<TData>\n  // ? DeepValue<TData, TAccessor>\n  // : never,\n  TType extends ColumnDataType,\n>(\n  accessor: TAccessor,\n  meta: Omit<ColumnMeta<TData, TVal>, 'type'> & {\n    type: TType\n  },\n): ColumnMeta<TData, TVal> {\n  return meta\n}\n\n/*\n * Represents a possible value for a column property of type 'option' or 'multiOption'.\n */\nexport interface ColumnOption {\n  /* The label to display for the option. */\n  label: string\n  /* The internal value of the option. */\n  value: string\n  /* An optional icon to display next to the label. */\n  icon?: React.ReactElement | React.ElementType\n}\n\n/*\n * Represents the data type of a column.\n */\nexport type ColumnDataType =\n  /* The column value is a string that should be searchable. */\n  | 'text'\n  | 'number'\n  | 'date'\n  /* The column value can be a single value from a list of options. */\n  | 'option'\n  /* The column value can be zero or more values from a list of options. */\n  | 'multiOption'\n\n/* Operators for text data */\nexport type TextFilterOperator = 'contains' | 'does not contain'\n\n/* Operators for number data */\nexport type NumberFilterOperator =\n  | 'is'\n  | 'is not'\n  | 'is less than'\n  | 'is greater than or equal to'\n  | 'is greater than'\n  | 'is less than or equal to'\n  | 'is between'\n  | 'is not between'\n\n/* Operators for date data */\nexport type DateFilterOperator =\n  | 'is'\n  | 'is not'\n  | 'is before'\n  | 'is on or after'\n  | 'is after'\n  | 'is on or before'\n  | 'is between'\n  | 'is not between'\n\n/* Operators for option data */\nexport type OptionFilterOperator = 'is' | 'is not' | 'is any of' | 'is none of'\n\n/* Operators for multi-option data */\nexport type MultiOptionFilterOperator =\n  | 'include'\n  | 'exclude'\n  | 'include any of'\n  | 'include all of'\n  | 'exclude if any of'\n  | 'exclude if all'\n\n/* Maps filter operators to their respective data types */\ntype FilterOperators = {\n  text: TextFilterOperator\n  number: NumberFilterOperator\n  date: DateFilterOperator\n  option: OptionFilterOperator\n  multiOption: MultiOptionFilterOperator\n}\n\n/* Maps filter values to their respective data types */\nexport type FilterTypes = {\n  text: string\n  number: number\n  date: Date\n  option: string\n  multiOption: string[]\n}\n\n/*\n *\n * FilterValue is a type that represents a filter value for a specific column.\n *\n * It consists of:\n * - Operator: The operator to be used for the filter.\n * - Values: An array of values to be used for the filter.\n *\n */\nexport type FilterValue<T extends ColumnDataType, TData> = {\n  operator: FilterOperators[T]\n  values: Array<FilterTypes[T]>\n  columnMeta: Column<TData>['columnDef']['meta']\n}\n\n/*\n * FilterDetails is a type that represents the details of all the filter operators for a specific column data type.\n */\nexport type FilterDetails<T extends ColumnDataType> = {\n  [key in FilterOperators[T]]: FilterOperatorDetails<key, T>\n}\n\ntype FilterOperatorDetailsBase<OperatorValue, T extends ColumnDataType> = {\n  /* The operator value. Usually the string representation of the operator. */\n  value: OperatorValue\n  /* The label for the operator, to show in the UI. */\n  label: string\n  /* How much data the operator applies to. */\n  target: 'single' | 'multiple'\n  /* The plural form of the operator, if applicable. */\n  singularOf?: FilterOperators[T]\n  /* The singular form of the operator, if applicable. */\n  pluralOf?: FilterOperators[T]\n  /* All related operators. Normally, all the operators which share the same target. */\n  relativeOf: FilterOperators[T] | Array<FilterOperators[T]>\n  /* Whether the operator is negated. */\n  isNegated: boolean\n  /* If the operator is not negated, this provides the negated equivalent. */\n  negation?: FilterOperators[T]\n  /* If the operator is negated, this provides the positive equivalent. */\n  negationOf?: FilterOperators[T]\n}\n\n/*\n *\n * FilterOperatorDetails is a type that provides details about a filter operator for a specific column data type.\n * It extends FilterOperatorDetailsBase with additional logic and contraints on the defined properties.\n *\n */\nexport type FilterOperatorDetails<\n  OperatorValue,\n  T extends ColumnDataType,\n> = FilterOperatorDetailsBase<OperatorValue, T> &\n  (\n    | { singularOf?: never; pluralOf?: never }\n    | { target: 'single'; singularOf: FilterOperators[T]; pluralOf?: never }\n    | { target: 'multiple'; singularOf?: never; pluralOf: FilterOperators[T] }\n  ) &\n  (\n    | { isNegated: false; negation: FilterOperators[T]; negationOf?: never }\n    | { isNegated: true; negation?: never; negationOf: FilterOperators[T] }\n  )\n\n/* Details for all the filter operators for option data type */\nexport const optionFilterDetails = {\n  is: {\n    label: 'is',\n    value: 'is',\n    target: 'single',\n    singularOf: 'is not',\n    relativeOf: 'is any of',\n    isNegated: false,\n    negation: 'is not',\n  },\n  'is not': {\n    label: 'is not',\n    value: 'is not',\n    target: 'single',\n    singularOf: 'is',\n    relativeOf: 'is none of',\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is any of': {\n    label: 'is any of',\n    value: 'is any of',\n    target: 'multiple',\n    pluralOf: 'is',\n    relativeOf: 'is',\n    isNegated: false,\n    negation: 'is none of',\n  },\n  'is none of': {\n    label: 'is none of',\n    value: 'is none of',\n    target: 'multiple',\n    pluralOf: 'is not',\n    relativeOf: 'is not',\n    isNegated: true,\n    negationOf: 'is any of',\n  },\n} as const satisfies FilterDetails<'option'>\n\n/* Details for all the filter operators for multi-option data type */\nexport const multiOptionFilterDetails = {\n  include: {\n    label: 'include',\n    value: 'include',\n    target: 'single',\n    singularOf: 'include any of',\n    relativeOf: 'exclude',\n    isNegated: false,\n    negation: 'exclude',\n  },\n  exclude: {\n    label: 'exclude',\n    value: 'exclude',\n    target: 'single',\n    singularOf: 'exclude if any of',\n    relativeOf: 'include',\n    isNegated: true,\n    negationOf: 'include',\n  },\n  'include any of': {\n    label: 'include any of',\n    value: 'include any of',\n    target: 'multiple',\n    pluralOf: 'include',\n    relativeOf: ['exclude if all', 'include all of', 'exclude if any of'],\n    isNegated: false,\n    negation: 'exclude if all',\n  },\n  'exclude if all': {\n    label: 'exclude if all',\n    value: 'exclude if all',\n    target: 'multiple',\n    pluralOf: 'exclude',\n    relativeOf: ['include any of', 'include all of', 'exclude if any of'],\n    isNegated: true,\n    negationOf: 'include any of',\n  },\n  'include all of': {\n    label: 'include all of',\n    value: 'include all of',\n    target: 'multiple',\n    pluralOf: 'include',\n    relativeOf: ['include any of', 'exclude if all', 'exclude if any of'],\n    isNegated: false,\n    negation: 'exclude if any of',\n  },\n  'exclude if any of': {\n    label: 'exclude if any of',\n    value: 'exclude if any of',\n    target: 'multiple',\n    pluralOf: 'exclude',\n    relativeOf: ['include any of', 'exclude if all', 'include all of'],\n    isNegated: true,\n    negationOf: 'include all of',\n  },\n} as const satisfies FilterDetails<'multiOption'>\n\n/* Details for all the filter operators for date data type */\nexport const dateFilterDetails = {\n  is: {\n    label: 'is',\n    value: 'is',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: 'is after',\n    isNegated: false,\n    negation: 'is before',\n  },\n  'is not': {\n    label: 'is not',\n    value: 'is not',\n    target: 'single',\n    singularOf: 'is not between',\n    relativeOf: [\n      'is',\n      'is before',\n      'is on or after',\n      'is after',\n      'is on or before',\n    ],\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is before': {\n    label: 'is before',\n    value: 'is before',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is on or after',\n      'is after',\n      'is on or before',\n    ],\n    isNegated: false,\n    negation: 'is on or after',\n  },\n  'is on or after': {\n    label: 'is on or after',\n    value: 'is on or after',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: ['is', 'is not', 'is before', 'is after', 'is on or before'],\n    isNegated: false,\n    negation: 'is before',\n  },\n  'is after': {\n    label: 'is after',\n    value: 'is after',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is before',\n      'is on or after',\n      'is on or before',\n    ],\n    isNegated: false,\n    negation: 'is on or before',\n  },\n  'is on or before': {\n    label: 'is on or before',\n    value: 'is on or before',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: ['is', 'is not', 'is after', 'is on or after', 'is before'],\n    isNegated: false,\n    negation: 'is after',\n  },\n  'is between': {\n    label: 'is between',\n    value: 'is between',\n    target: 'multiple',\n    pluralOf: 'is',\n    relativeOf: 'is not between',\n    isNegated: false,\n    negation: 'is not between',\n  },\n  'is not between': {\n    label: 'is not between',\n    value: 'is not between',\n    target: 'multiple',\n    pluralOf: 'is not',\n    relativeOf: 'is between',\n    isNegated: true,\n    negationOf: 'is between',\n  },\n} as const satisfies FilterDetails<'date'>\n\n/* Details for all the filter operators for text data type */\nexport const textFilterDetails = {\n  contains: {\n    label: 'contains',\n    value: 'contains',\n    target: 'single',\n    relativeOf: 'does not contain',\n    isNegated: false,\n    negation: 'does not contain',\n  },\n  'does not contain': {\n    label: 'does not contain',\n    value: 'does not contain',\n    target: 'single',\n    relativeOf: 'contains',\n    isNegated: true,\n    negationOf: 'contains',\n  },\n} as const satisfies FilterDetails<'text'>\n\n/* Details for all the filter operators for number data type */\nexport const numberFilterDetails = {\n  is: {\n    label: 'is',\n    value: 'is',\n    target: 'single',\n    relativeOf: [\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is not',\n  },\n  'is not': {\n    label: 'is not',\n    value: 'is not',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is greater than': {\n    label: '>',\n    value: 'is greater than',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is less than or equal to',\n  },\n  'is greater than or equal to': {\n    label: '>=',\n    value: 'is greater than or equal to',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n    ],\n    isNegated: false,\n    negation: 'is less than or equal to',\n  },\n  'is less than': {\n    label: '<',\n    value: 'is less than',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is greater than',\n  },\n  'is less than or equal to': {\n    label: '<=',\n    value: 'is less than or equal to',\n    target: 'single',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is greater than or equal to',\n  },\n  'is between': {\n    label: 'is between',\n    value: 'is between',\n    target: 'multiple',\n    relativeOf: 'is not between',\n    isNegated: false,\n    negation: 'is not between',\n  },\n  'is not between': {\n    label: 'is not between',\n    value: 'is not between',\n    target: 'multiple',\n    relativeOf: 'is between',\n    isNegated: true,\n    negationOf: 'is between',\n  },\n} as const satisfies FilterDetails<'number'>\n\n/* Maps column data types to their respective filter operator details */\ntype FilterTypeOperatorDetails = {\n  [key in ColumnDataType]: FilterDetails<key>\n}\n\nexport const filterTypeOperatorDetails: FilterTypeOperatorDetails = {\n  text: textFilterDetails,\n  number: numberFilterDetails,\n  date: dateFilterDetails,\n  option: optionFilterDetails,\n  multiOption: multiOptionFilterDetails,\n}\n\n/*\n *\n * Determines the new operator for a filter based on the current operator, old and new filter values.\n *\n * This handles cases where the filter values have transitioned from a single value to multiple values (or vice versa),\n * and the current operator needs to be transitioned to its plural form (or singular form).\n *\n * For example, if the current operator is 'is', and the new filter values have a length of 2, the\n * new operator would be 'is any of'.\n *\n */\nexport function determineNewOperator<T extends ColumnDataType>(\n  type: T,\n  oldVals: Array<FilterTypes[T]>,\n  nextVals: Array<FilterTypes[T]>,\n  currentOperator: FilterOperators[T],\n): FilterOperators[T] {\n  const a =\n    Array.isArray(oldVals) && Array.isArray(oldVals[0])\n      ? oldVals[0].length\n      : oldVals.length\n  const b =\n    Array.isArray(nextVals) && Array.isArray(nextVals[0])\n      ? nextVals[0].length\n      : nextVals.length\n\n  // If filter size has not transitioned from single to multiple (or vice versa)\n  // or is unchanged, return the current operator.\n  if (a === b || (a >= 2 && b >= 2) || (a <= 1 && b <= 1))\n    return currentOperator\n\n  const opDetails = filterTypeOperatorDetails[type][currentOperator]\n\n  // Handle transition from single to multiple filter values.\n  if (a < b && b >= 2) return opDetails.singularOf ?? currentOperator\n  // Handle transition from multiple to single filter values.\n  if (a > b && b <= 1) return opDetails.pluralOf ?? currentOperator\n  return currentOperator\n}\n\n/**********************************************************************************************************\n ***** Filter Functions ******\n **********************************************************************************************************\n * These are functions that filter data based on the current filter values, column data type, and operator.\n * There exists a separate filter function for each column data type.\n *\n * Two variants of the filter functions are provided - as an example, we will take the optionFilterFn:\n * 1. optionFilterFn: takes in a row, columnId, and filterValue.\n * 2. __optionFilterFn: takes in an inputData and filterValue.\n *\n * __optionFilterFn is a private function that is used by filterFn to perform the actual filtering.\n * *********************************************************************************************************/\n\n/*\n * Returns a filter function for a given column data type.\n * This function is used to determine the appropriate filter function to use based on the column data type.\n */\nexport function filterFn(dataType: ColumnDataType) {\n  switch (dataType) {\n    case 'option':\n      return optionFilterFn\n    case 'multiOption':\n      return multiOptionFilterFn\n    case 'date':\n      return dateFilterFn\n    case 'text':\n      return textFilterFn\n    case 'number':\n      return numberFilterFn\n    default:\n      throw new Error('Invalid column data type')\n  }\n}\n\nexport function optionFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'option', TData>,\n) {\n  const value = row.getValue(columnId)\n\n  if (!value) return false\n\n  const columnMeta = filterValue.columnMeta!\n\n  if (typeof value === 'string') {\n    return __optionFilterFn(value, filterValue)\n  }\n\n  if (isColumnOption(value)) {\n    return __optionFilterFn(value.value, filterValue)\n  }\n\n  const sanitizedValue = columnMeta.transformOptionFn!(value as never)\n  return __optionFilterFn(sanitizedValue.value, filterValue)\n}\n\nexport function __optionFilterFn<TData>(\n  inputData: string,\n  filterValue: FilterValue<'option', TData>,\n) {\n  if (!inputData) return false\n  if (filterValue.values.length === 0) return true\n\n  const value = inputData.toString().toLowerCase()\n\n  const found = !!filterValue.values.find((v) => v.toLowerCase() === value)\n\n  switch (filterValue.operator) {\n    case 'is':\n    case 'is any of':\n      return found\n    case 'is not':\n    case 'is none of':\n      return !found\n  }\n}\n\nexport function isColumnOption(value: unknown): value is ColumnOption {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    'value' in value &&\n    'label' in value\n  )\n}\n\nexport function isColumnOptionArray(value: unknown): value is ColumnOption[] {\n  return Array.isArray(value) && value.every(isColumnOption)\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n  return Array.isArray(value) && value.every((v) => typeof v === 'string')\n}\n\nexport function multiOptionFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'multiOption', TData>,\n) {\n  const value = row.getValue(columnId)\n\n  if (!value) return false\n\n  const columnMeta = filterValue.columnMeta!\n\n  if (isStringArray(value)) {\n    return __multiOptionFilterFn(value, filterValue)\n  }\n\n  if (isColumnOptionArray(value)) {\n    return __multiOptionFilterFn(\n      value.map((v) => v.value),\n      filterValue,\n    )\n  }\n\n  const sanitizedValue = (value as never[]).map((v) =>\n    columnMeta.transformOptionFn!(v),\n  )\n\n  return __multiOptionFilterFn(\n    sanitizedValue.map((v) => v.value),\n    filterValue,\n  )\n}\n\nexport function __multiOptionFilterFn<TData>(\n  inputData: string[],\n  filterValue: FilterValue<'multiOption', TData>,\n) {\n  if (!inputData) return false\n\n  if (\n    filterValue.values.length === 0 ||\n    !filterValue.values[0] ||\n    filterValue.values[0].length === 0\n  )\n    return true\n\n  const values = uniq(inputData)\n  const filterValues = uniq(filterValue.values[0])\n\n  switch (filterValue.operator) {\n    case 'include':\n    case 'include any of':\n      return intersection(values, filterValues).length > 0\n    case 'exclude':\n      return intersection(values, filterValues).length === 0\n    case 'exclude if any of':\n      return !(intersection(values, filterValues).length > 0)\n    case 'include all of':\n      return intersection(values, filterValues).length === filterValues.length\n    case 'exclude if all':\n      return !(\n        intersection(values, filterValues).length === filterValues.length\n      )\n  }\n}\n\nexport function dateFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'date', TData>,\n) {\n  const valueStr = row.getValue<Date>(columnId)\n\n  return __dateFilterFn(valueStr, filterValue)\n}\n\nexport function __dateFilterFn<TData>(\n  inputData: Date,\n  filterValue: FilterValue<'date', TData>,\n) {\n  if (!filterValue || filterValue.values.length === 0) return true\n\n  if (\n    dateFilterDetails[filterValue.operator].target === 'single' &&\n    filterValue.values.length > 1\n  )\n    throw new Error('Singular operators require at most one filter value')\n\n  if (\n    filterValue.operator in ['is between', 'is not between'] &&\n    filterValue.values.length !== 2\n  )\n    throw new Error('Plural operators require two filter values')\n\n  const filterVals = filterValue.values\n  const d1 = filterVals[0]\n  const d2 = filterVals[1]\n\n  const value = inputData\n\n  switch (filterValue.operator) {\n    case 'is':\n      return isSameDay(value, d1)\n    case 'is not':\n      return !isSameDay(value, d1)\n    case 'is before':\n      return isBefore(value, startOfDay(d1))\n    case 'is on or after':\n      return isSameDay(value, d1) || isAfter(value, startOfDay(d1))\n    case 'is after':\n      return isAfter(value, startOfDay(d1))\n    case 'is on or before':\n      return isSameDay(value, d1) || isBefore(value, startOfDay(d1))\n    case 'is between':\n      return isWithinInterval(value, {\n        start: startOfDay(d1),\n        end: endOfDay(d2),\n      })\n    case 'is not between':\n      return !isWithinInterval(value, {\n        start: startOfDay(filterValue.values[0]),\n        end: endOfDay(filterValue.values[1]),\n      })\n  }\n}\n\nexport function textFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'text', TData>,\n) {\n  const value = row.getValue<string>(columnId) ?? ''\n\n  return __textFilterFn(value, filterValue)\n}\n\nexport function __textFilterFn<TData>(\n  inputData: string,\n  filterValue: FilterValue<'text', TData>,\n) {\n  if (!filterValue || filterValue.values.length === 0) return true\n\n  const value = inputData.toLowerCase().trim()\n  const filterStr = filterValue.values[0].toLowerCase().trim()\n\n  if (filterStr === '') return true\n\n  const found = value.includes(filterStr)\n\n  switch (filterValue.operator) {\n    case 'contains':\n      return found\n    case 'does not contain':\n      return !found\n  }\n}\n\nexport function numberFilterFn<TData>(\n  row: Row<TData>,\n  columnId: string,\n  filterValue: FilterValue<'number', TData>,\n) {\n  const value = row.getValue<number>(columnId)\n\n  return __numberFilterFn(value, filterValue)\n}\n\nexport function __numberFilterFn<TData>(\n  inputData: number,\n  filterValue: FilterValue<'number', TData>,\n) {\n  if (!filterValue || !filterValue.values || filterValue.values.length === 0) {\n    return true\n  }\n\n  const value = inputData\n  const filterVal = filterValue.values[0]\n\n  switch (filterValue.operator) {\n    case 'is':\n      return value === filterVal\n    case 'is not':\n      return value !== filterVal\n    case 'is greater than':\n      return value > filterVal\n    case 'is greater than or equal to':\n      return value >= filterVal\n    case 'is less than':\n      return value < filterVal\n    case 'is less than or equal to':\n      return value <= filterVal\n    case 'is between': {\n      const lowerBound = filterValue.values[0]\n      const upperBound = filterValue.values[1]\n      return value >= lowerBound && value <= upperBound\n    }\n    case 'is not between': {\n      const lowerBound = filterValue.values[0]\n      const upperBound = filterValue.values[1]\n      return value < lowerBound || value > upperBound\n    }\n    default:\n      return true\n  }\n}\n\nexport function createNumberRange(values: number[] | undefined) {\n  let a = 0\n  let b = 0\n\n  if (!values || values.length === 0) return [a, b]\n  if (values.length === 1) {\n    a = values[0]\n  } else {\n    a = values[0]\n    b = values[1]\n  }\n\n  const [min, max] = a < b ? [a, b] : [b, a]\n\n  return [min, max]\n}\n\n/*** Table helpers ***/\n\nexport function getColumn<TData>(table: Table<TData>, id: string) {\n  const column = table.getColumn(id)\n\n  if (!column) {\n    throw new Error(`Column with id ${id} not found`)\n  }\n\n  return column\n}\n\nexport function getColumnMeta<TData>(table: Table<TData>, id: string) {\n  const column = getColumn(table, id)\n\n  if (!column.columnDef.meta) {\n    throw new Error(`Column meta not found for column ${id}`)\n  }\n\n  return column.columnDef.meta\n}\n",
       "type": "registry:lib"
     }
   ]
diff --git a/apps/web/public/site.webmanifest b/apps/web/public/site.webmanifest
index 45dc8a20..fa99de77 100644
--- a/apps/web/public/site.webmanifest
+++ b/apps/web/public/site.webmanifest
@@ -1 +1,19 @@
-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
+{
+  "name": "",
+  "short_name": "",
+  "icons": [
+    {
+      "src": "/android-chrome-192x192.png",
+      "sizes": "192x192",
+      "type": "image/png"
+    },
+    {
+      "src": "/android-chrome-512x512.png",
+      "sizes": "512x512",
+      "type": "image/png"
+    }
+  ],
+  "theme_color": "#ffffff",
+  "background_color": "#ffffff",
+  "display": "standalone"
+}
diff --git a/apps/web/registry/data-table-filter/lib/filters.test.ts b/apps/web/registry/data-table-filter/lib/filters.test.ts
new file mode 100644
index 00000000..9e15e668
--- /dev/null
+++ b/apps/web/registry/data-table-filter/lib/filters.test.ts
@@ -0,0 +1,48 @@
+// @ts-nocheck
+import { defineMeta } from '@/registry/data-table-filter/lib/filters'
+import { createColumnHelper } from '@tanstack/react-table'
+import { PersonStandingIcon } from 'lucide-react'
+
+type Person = {
+  name: {
+    first: string
+    middle?: string
+    last: string
+  }
+  fullName: string
+  age: number
+  gender: 'male' | 'female'
+  birthday: Date
+  email: string
+  salary?: number
+  bio: string
+}
+
+const columnHelper = createColumnHelper<Person>()
+
+const columns = [
+  columnHelper.accessor('fullName', {
+    id: 'fullName1',
+    // Uses accessorKey
+    meta: defineMeta('fullName', {
+      type: 'option',
+      icon: PersonStandingIcon,
+      displayName: 'Full name',
+      transformOptionFn(value) {
+        return { value, label: value }
+      },
+    }),
+  }),
+  columnHelper.accessor('fullName', {
+    id: 'fullName2',
+    // Uses accessorFn
+    meta: defineMeta((row) => row.fullName, {
+      type: 'option',
+      icon: PersonStandingIcon,
+      displayName: 'Full name',
+      transformOptionFn(value) {
+        return { value, label: value }
+      },
+    }),
+  }),
+]
diff --git a/apps/web/registry/data-table-filter/lib/filters.ts b/apps/web/registry/data-table-filter/lib/filters.ts
index f38aa341..5966133e 100644
--- a/apps/web/registry/data-table-filter/lib/filters.ts
+++ b/apps/web/registry/data-table-filter/lib/filters.ts
@@ -1,5 +1,12 @@
 import '@tanstack/table-core'
-import type { Column, Row, RowData } from '@tanstack/react-table'
+import type {
+  AccessorFn,
+  Column,
+  DeepKeys,
+  DeepValue,
+  Row,
+  RowData,
+} from '@tanstack/react-table'
 import type { ColumnMeta, Table } from '@tanstack/react-table'
 import {
   endOfDay,
@@ -43,16 +50,31 @@ declare module '@tanstack/react-table' {
   }
 }
 
+/* TODO: Allow both accessorFn and accessorKey */
 export function defineMeta<
   TData,
-  TKey extends keyof TData,
+  /* Only accessorFn - WORKS */
+  TAccessor extends AccessorFn<TData>,
+  TVal extends ReturnType<TAccessor>,
+  /* Only accessorKey - WORKS */
+  // TAccessor extends DeepKeys<TData>,
+  // TVal extends DeepValue<TData, TAccessor>,
+
+  /* Both accessorKey and accessorFn - BROKEN */
+  /* ISSUE: Won't infer transformOptionFn input type correctly. */
+  // TAccessor extends AccessorFn<TData> | DeepKeys<TData>,
+  // TVal extends TAccessor extends AccessorFn<TData>
+  // ? ReturnType<TAccessor>
+  // : TAccessor extends DeepKeys<TData>
+  // ? DeepValue<TData, TAccessor>
+  // : never,
   TType extends ColumnDataType,
 >(
-  key: TKey,
-  meta: Omit<ColumnMeta<TData, TData[TKey]>, 'type'> & {
+  accessor: TAccessor,
+  meta: Omit<ColumnMeta<TData, TVal>, 'type'> & {
     type: TType
   },
-): ColumnMeta<TData, TData[TKey]> {
+): ColumnMeta<TData, TVal> {
   return meta
 }
 
diff --git a/bun.lock b/bun.lock
index b6e18288..1c26e6fc 100644
--- a/bun.lock
+++ b/bun.lock
@@ -100,7 +100,7 @@
         "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
         "cmdk": "^1.1.1",
-        "date-fns": "4.1.0",
+        "date-fns": "^4.1.0",
         "lucide-react": "^0.481.0",
         "nanoid": "^5.1.3",
         "react": "^19.0.0",
diff --git a/examples/vite-basic/package.json b/examples/vite-basic/package.json
index bb9fdec4..4e43f4fc 100644
--- a/examples/vite-basic/package.json
+++ b/examples/vite-basic/package.json
@@ -29,7 +29,7 @@
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
     "cmdk": "^1.1.1",
-    "date-fns": "4.1.0",
+    "date-fns": "^4.1.0",
     "lucide-react": "^0.481.0",
     "nanoid": "^5.1.3",
     "react": "^19.0.0",
diff --git a/examples/vite-basic/src/components/demo/dynamic/columns.tsx b/examples/vite-basic/src/components/demo/dynamic/columns.tsx
index 966668fc..ea447c69 100644
--- a/examples/vite-basic/src/components/demo/dynamic/columns.tsx
+++ b/examples/vite-basic/src/components/demo/dynamic/columns.tsx
@@ -14,7 +14,6 @@ import {
   TagsIcon,
   UserCheckIcon,
 } from 'lucide-react'
-import { USERS } from '../shared/data'
 import type { Issue } from './types'
 
 const columnHelper = createColumnHelper<Issue>()
@@ -146,6 +145,20 @@ export const columns = [
       }),
     },
   }),
+  columnHelper.accessor((row) => row.assignee?.name, {
+    id: 'assigneeName',
+    header: 'Assignee (Name)',
+    filterFn: filterFn('option'),
+    meta: defineMeta((row) => row.assignee?.name, {
+      displayName: 'Assignee (Name)',
+      type: 'option',
+      icon: UserCheckIcon,
+      transformOptionFn: (firstName) => ({
+        value: firstName,
+        label: firstName,
+      }),
+    }),
+  }),
   columnHelper.accessor((row) => row.estimatedHours, {
     id: 'estimatedHours',
     header: 'Estimated Hours',
@@ -241,7 +254,7 @@ export const columns = [
       )
     },
     filterFn: filterFn('multiOption'),
-    meta: defineMeta('labels', {
+    meta: defineMeta((row) => row.labels, {
       displayName: 'Labels',
       type: 'multiOption',
       icon: TagsIcon,
diff --git a/examples/vite-basic/src/components/demo/ex1/columns.tsx b/examples/vite-basic/src/components/demo/ex1/columns.tsx
index 66d20ac7..61cec10c 100644
--- a/examples/vite-basic/src/components/demo/ex1/columns.tsx
+++ b/examples/vite-basic/src/components/demo/ex1/columns.tsx
@@ -25,7 +25,7 @@ export const columns: ColumnDef<Issue, any>[] = [
     id: 'title',
     header: 'Title',
     filterFn: filterFn('text'),
-    meta: defineMeta('title', {
+    meta: defineMeta((row) => row.title, {
       displayName: 'Title',
       type: 'text',
       icon: Heading1Icon,
@@ -35,7 +35,7 @@ export const columns: ColumnDef<Issue, any>[] = [
     id: 'dueDate',
     header: 'Due Date',
     filterFn: filterFn('date'),
-    meta: defineMeta('dueDate', {
+    meta: defineMeta((row) => row.dueDate, {
       displayName: 'Due Date',
       type: 'date',
       icon: CalendarIcon,
@@ -45,7 +45,7 @@ export const columns: ColumnDef<Issue, any>[] = [
     id: 'estimatedHours',
     header: 'Estimated Hours',
     filterFn: filterFn('number'),
-    meta: defineMeta('estimatedHours', {
+    meta: defineMeta((row) => row.estimatedHours, {
       displayName: 'Estimated Hours',
       type: 'number',
       icon: ClockIcon,
@@ -55,7 +55,7 @@ export const columns: ColumnDef<Issue, any>[] = [
     id: 'status',
     header: 'Status',
     filterFn: filterFn('option'),
-    meta: defineMeta('status', {
+    meta: defineMeta((row) => row.status, {
       displayName: 'Status',
       type: 'option',
       icon: CircleDotDashedIcon,
diff --git a/examples/vite-basic/src/lib/filters.ts b/examples/vite-basic/src/lib/filters.ts
index f38aa341..5966133e 100644
--- a/examples/vite-basic/src/lib/filters.ts
+++ b/examples/vite-basic/src/lib/filters.ts
@@ -1,5 +1,12 @@
 import '@tanstack/table-core'
-import type { Column, Row, RowData } from '@tanstack/react-table'
+import type {
+  AccessorFn,
+  Column,
+  DeepKeys,
+  DeepValue,
+  Row,
+  RowData,
+} from '@tanstack/react-table'
 import type { ColumnMeta, Table } from '@tanstack/react-table'
 import {
   endOfDay,
@@ -43,16 +50,31 @@ declare module '@tanstack/react-table' {
   }
 }
 
+/* TODO: Allow both accessorFn and accessorKey */
 export function defineMeta<
   TData,
-  TKey extends keyof TData,
+  /* Only accessorFn - WORKS */
+  TAccessor extends AccessorFn<TData>,
+  TVal extends ReturnType<TAccessor>,
+  /* Only accessorKey - WORKS */
+  // TAccessor extends DeepKeys<TData>,
+  // TVal extends DeepValue<TData, TAccessor>,
+
+  /* Both accessorKey and accessorFn - BROKEN */
+  /* ISSUE: Won't infer transformOptionFn input type correctly. */
+  // TAccessor extends AccessorFn<TData> | DeepKeys<TData>,
+  // TVal extends TAccessor extends AccessorFn<TData>
+  // ? ReturnType<TAccessor>
+  // : TAccessor extends DeepKeys<TData>
+  // ? DeepValue<TData, TAccessor>
+  // : never,
   TType extends ColumnDataType,
 >(
-  key: TKey,
-  meta: Omit<ColumnMeta<TData, TData[TKey]>, 'type'> & {
+  accessor: TAccessor,
+  meta: Omit<ColumnMeta<TData, TVal>, 'type'> & {
     type: TType
   },
-): ColumnMeta<TData, TData[TKey]> {
+): ColumnMeta<TData, TVal> {
   return meta
 }
 


Reviews:
user: login: sapter
body: Newly added properties to user are unused.
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-01T14:52:49Z
user: login: kianbazza
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:07:31Z
user: login: kianbazza
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:15:41Z
user: login: kianbazza
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:15:46Z
user: login: kianbazza
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-01T15:23:59Z

Review comments:
diff_hunk: @@ -5,21 +5,29 @@ export const users: User[] = [
   {
     id: 'a1b2c3d4e5f6g7h8i9j0k',
     name: 'John Smith',
+    firstName: 'John',
path: apps/web/components/data-table-filter/data.ts
user: login: sapter
body: You don't seem to be using these 2 new added properties anywhere. You're still using `assignee.name` in your column definitions and meta objects...
created_at: 2025-04-01T14:41:00Z
author_association: NONE
diff_hunk: @@ -212,6 +212,20 @@ export const columns = [
       }),
     }),
   }),
+  columnHelper.accessor('assignee.name', {
+    id: 'assigneeName',
+    header: 'Assignee (Name)',
+    filterFn: filterFn('option'),
+    meta: defineMeta('assignee.name', {
+      displayName: 'Assignee (Name)',
+      type: 'option',
+      icon: UserCheckIcon,
+      transformOptionFn: (firstName) => ({
path: apps/web/components/data-table-filter/demo.tsx
user: login: sapter
body: You're accessing the full name `assignee.name` – not the `firstName`. 
If you just want the name, you can just modify the previous column's accessor key to you `assignee.name` and get rid of this new column. If you want the first name only, change the accessor key here to `assignee.firstName`
created_at: 2025-04-01T14:47:22Z
author_association: NONE
diff_hunk: @@ -5,21 +5,29 @@ export const users: User[] = [
   {
     id: 'a1b2c3d4e5f6g7h8i9j0k',
     name: 'John Smith',
+    firstName: 'John',
path: apps/web/components/data-table-filter/data.ts
user: login: kianbazza
body: Ah shoot, good catch. Starting doing one thing, then pivoted and forgot to cleanup.
created_at: 2025-04-01T15:07:31Z
author_association: COLLABORATOR
diff_hunk: @@ -212,6 +212,20 @@ export const columns = [
       }),
     }),
   }),
+  columnHelper.accessor('assignee.name', {
+    id: 'assigneeName',
+    header: 'Assignee (Name)',
+    filterFn: filterFn('option'),
+    meta: defineMeta('assignee.name', {
+      displayName: 'Assignee (Name)',
+      type: 'option',
+      icon: UserCheckIcon,
+      transformOptionFn: (firstName) => ({
path: apps/web/components/data-table-filter/demo.tsx
user: login: kianbazza
body: Done.
created_at: 2025-04-01T15:15:41Z
author_association: COLLABORATOR
diff_hunk: @@ -5,21 +5,29 @@ export const users: User[] = [
   {
     id: 'a1b2c3d4e5f6g7h8i9j0k',
     name: 'John Smith',
+    firstName: 'John',
path: apps/web/components/data-table-filter/data.ts
user: login: kianbazza
body: Cleaned up.
created_at: 2025-04-01T15:15:46Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-01T09:48:15Z
body: @Drosscend is attempting to deploy a commit to the **BazzaDEV** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=BazzaDEV&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22e958100259a43c9a33c6b9d3440e47e45c3a9b10%22%7D%2C%22id%22%3A%22QmeDXhBkMQXsAMZruSbbQhfR9ef6WkbRxjwHna9ioehLy3%22%2C%22org%22%3A%22kianbazza%22%2C%22prId%22%3A28%2C%22repo%22%3A%22ui%22%7D).


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-01T10:47:31Z
body: [vc]: #gQdkhx2/5zNDk49sKK2Myra5zkMiokD42TSq0OGweq0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ1aSIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9iYXp6YWxhYnMvdWkvRUpRNHBUem5jMjlxNndnaWNqTHdFblhqUGVSZSIsInByZXZpZXdVcmwiOiJ1aS1naXQtZm9yay1kcm9zc2NlbmQtZmVhdC1uZXN0ZWQtcGF0aC1iYXp6YWxhYnMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ1aS1naXQtZm9yay1kcm9zc2NlbmQtZmVhdC1uZXN0ZWQtcGF0aC1iYXp6YWxhYnMudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **ui** | ✅ Ready ([Inspect](https://vercel.com/bazzalabs/ui/EJQ4pTznc29q6wgicjLwEnXjPeRe)) | [Visit Preview](https://ui-git-fork-drosscend-feat-nested-path-bazzalabs.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/ui-git-fork-drosscend-feat-nested-path-bazzalabs.vercel.app?via=pr-comment-feedback-link) | Apr 2, 2025 3:02am |


author_association: NONE
user: login: kianbazza
created_at: 2025-04-01T11:34:42Z
body: @Drosscend Good idea with this PR.

Your implementation wasn't working, I tried to extend the demo data columns with assignee.name but was getting type errors.

Luckily, TanStack Table already the `DeepKeys` and `DeepValue` helper types which does the same thing:

https://github.com/TanStack/table/blob/f7bf6f1adfa4f8b28b9968b29745f2452d4be9d8/packages/table-core/src/utils.ts#L46

This fixed the type errors and deep-keyed properties are filterable now.

However, there's a big performance issue - not sure if this just wasn't tested on my end. The vite-basic example (dynamic tab) is loading 5000 rows. Opening the filter menu for the deep-keyed property takes a few seconds to load.

Need to investigate this further. If you can help out, please do :) I appreciate the help with this!
author_association: COLLABORATOR
user: login: Drosscend
created_at: 2025-04-01T13:10:06Z
body: Thank you for your prompt response! I really appreciate you taking the time to help me with this issue.

Regarding the `DeepKeys` and `DeepValue` types from TanStack Table that you mentioned. I have implemented them in my project, and they integrate perfectly with my existing code, achieving the same functionality as my previous implementation.

Regarding the performance issue, I'll continue investigating. If I find a solution to fix the performance problem, I'll definitely let you know. I believe there must be a way to optimize this without sacrificing functionality.

Thanks again for your help!
author_association: CONTRIBUTOR
user: login: sapter
created_at: 2025-04-01T15:01:42Z
body: @kianbazza the performance issues you're having in the vite-basic dynamic sample are likely due to half the issues having no defined assignee. You can't access (deep) keys of undefined objects. (accessor key `assignee.name` is invalid if `assignee` may be undefined)
https://github.com/kianbazza/ui/blob/main/examples/vite-basic/src/components/demo/dynamic/data.ts#L29  
author_association: NONE
user: login: kianbazza
created_at: 2025-04-01T15:23:08Z
body: <!-- linear:replyTo:2732954260 -->

Hmm, seems like using an accessorKey for this specific use case is not the way to go, according to this: 

[How to suppress/disable "deeply nested key returned undefined" warning · TanStack table · Discussion #4788](https://github.com/TanStack/table/discussions/4788#discussioncomment-7285131)

For context, the reason why we need to specify the accessor in the `defineMeta` function is for type safety. That parameter doesn't actually get used directly. It's used as a hint to the TS compiler to infer the correct type for ColumnMeta.

ColumnMeta takes two generics `TData` and `TValue`. The first was being inferred correctly, but `TValue` was not. Adding this "dummy parameter" fixes this.

We could simply require the user to pass in the same `accessorFn` they use to define the column again. But that seems too redundant, especially if it's a longer definition.

Ideally, I want to drop the initial parameter from `defineMeta` altogether.
author_association: COLLABORATOR
user: login: sapter
created_at: 2025-04-01T15:53:52Z
body: > Hmm, seems like using an accessorKey for this specific use case is not the way to go, according to this:
> 
> [How to suppress/disable "deeply nested key returned undefined" warning · TanStack table · Discussion #4788](https://github.com/TanStack/table/discussions/4788#discussioncomment-7285131)
> 
> For context, the reason why we need to specify the accessor in the `defineMeta` function is for type safety. That parameter doesn't actually get used directly. It's used as a hint to the TS compiler to infer the correct type for ColumnMeta.
> 
> ColumnMeta takes two generics `TData` and `TValue`. The first was being inferred correctly, but `TValue` was not. Adding this "dummy parameter" fixes this.
> 
> We could simply require the user to pass in the same `accessorFn` they use to define the column again. But that seems too redundant, especially if it's a longer definition.
> 
> Ideally, I want to drop the initial parameter from `defineMeta` altogether.

Your performance issues in the vite-basic dynamic sample are not because of what you pass to `defineMeta` but because of roughly 50% of your 5000 rows having an undefined `assignee` property. TanstackTable (TST) does use the accessorKey to actually access the data for the row.  So first try changing the code to use an acessorFN rather than the accessorKey (as the thread above suggests and as specified in TST docs) to see if the performance issue goes away and then we can busy ourselves with trying to figure out a more ergonomic API footprint for types and helper functions.
author_association: NONE
user: login: kianbazza
created_at: 2025-04-01T15:55:41Z
body: <!-- linear:replyTo:2732954260 -->

For sure. My comment was more of a sidebar 🙂 Will give this a go now. Appreciate your help with this!
author_association: COLLABORATOR
user: login: sapter
created_at: 2025-04-01T16:24:08Z
body: In general, for an ergonomic API that's also flexible and open to future expansion and addition of features (like server-side filtering, for example), I'd look into decoupling the filter components from the table definition. Instead, the components could be just UI components, each of which could be output in the shape of the column filter state TST uses (https://tanstack.com/table/latest/docs/api/features/column-filtering#state). The component could have its own logic and some internal state in addition to the TST filter state. The source/storage of the state could be anything (useState, ReactContext, useStore, Zustand, or the URL/NUQS/SearchParams).

It should be up to the user to then share that state with TST. We can of course show a nice range of examples of how to do so, but they're just that - examples. In essence, the app has some filter state that both the filter components and TST share. The shape of that state is that which is defined in the TST docs.

Thoughts?
author_association: NONE
user: login: kianbazza
created_at: 2025-04-01T16:26:46Z
body: <!-- linear:replyTo:2732954260 -->

Oh 100%, my next priority is adding server-side filtering and a prerequisite to that will be decoupling the UI and filtering logic.

FYI we have a public feedback board and this seems popular:[ https://ui.bazza.dev/feedback](https://ui.bazza.dev/feedback) 
author_association: COLLABORATOR
user: login: sapter
created_at: 2025-04-01T16:31:48Z
body: Yes, I commented there as Sam A 😉
author_association: NONE
user: login: kianbazza
created_at: 2025-04-01T20:35:46Z
body: <!-- linear:replyTo:2732954260 -->

@sapter I'm hitting some roadblocks making both `accessorKey` and `accessorFn` valid inputs. Separately, they work. Trying to allow both results in types not being inferred correctly for `transformOptionFn`.

```ts
export function defineMeta<
  TData,
  /* Only accessorFn - WORKS */
  // TAccessor extends AccessorFn<TData>,
  // TVal extends ReturnType<TAccessor>,

  /* Only accessorKey - WORKS */
  // TAccessor extends DeepKeys<TData>,
  // TVal extends DeepValue<TData, TAccessor>,

  /* Both accessorKey and accessorFn - BROKEN */
  /* Cannot infer argument type for `transformOptionFn` correctly */
  // TAccessor extends AccessorFn<TData> | DeepKeys<TData>,
  // TVal extends TAccessor extends AccessorFn<TData>
  // ? ReturnType<TAccessor>
  // : TAccessor extends DeepKeys<TData>
  // ? DeepValue<TData, TAccessor>
  // : never,
  TType extends ColumnDataType,
>;
```

Thoughts?

One option is to just enforce `accessorFn` usage. My latest commit shows this. Let me know.
author_association: COLLABORATOR
user: login: sapter
created_at: 2025-04-01T21:55:00Z
body: I think for now requiring one or the other is fine as it may not be worth investing too much time in wrestling with types if you plan to change the API anyway.

Have a look at the way TST implemented their own filters https://github.com/TanStack/table/blob/main/examples/react/filters/src/main.tsx#L247

1. They get the meta from the column which is passed as props into the filter
2. To support server-side filtering, you could make the column prop optional (so only client-side uses it). For server-side filtering, all your component does is set the URL. Once the URL changes, the data changes; the data already comes filtered from the server.
author_association: NONE
user: login: kianbazza
created_at: 2025-04-01T22:01:22Z
body: <!-- linear:replyTo:2732954260 -->

Okay great. Let's take the rest of this into a separate discussion. I'll done some final review and merge these changes.
author_association: COLLABORATOR
user: login: kianbazza
created_at: 2025-04-01T22:34:54Z
body: <!-- linear:isThreadRoot -->

Also, I didn't directly say it, but I can confirm that using `accessorFn` means there's no performance hit anymore :)
author_association: COLLABORATOR