# ObjectTable

A comprehensive guide for using the ObjectTable component from `@osdk/react-components`.

## Prerequisites

Before using ObjectTable, make sure you have completed the library setup described in the [README](https://github.com/palantir/osdk-ts/blob/main/packages/react-components/README.md#setup), including:

- Installing the required dependencies
- Wrapping your app with `OsdkProvider`
- Adding the CSS imports

## Table of Contents

- [Basic Usage](#basic-usage)
- [Props Reference](#props-reference)
- [Column Definitions](#column-definitions)
- [Examples](#examples)
- [Advanced Features](#advanced-features)
- [TypeScript Tips](#typescript-tips)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
- [Theming](#theming)

## Import

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import type {
  ColumnDefinition,
  EditFieldConfig,
} from "@osdk/react-components/experimental/object-table";
```

## Basic Usage

### Minimal Example

The simplest way to use ObjectTable is with just an object type:

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import { Office } from "@YourApp/sdk";

function OfficesPage() {
  return (
    <ObjectTable
      objectType={Office}
    />
  );
}
```

This displays all properties of the Office object type in a table with default settings.

### With Selection

Add selection mode to enable row selection:

```typescript
<ObjectTable
  objectType={Office}
  selectionMode="single" // or "multiple" or "none" (default)
/>;
```

## Props Reference

### Core Props

| Prop         | Type     | Required | Default | Description                     |
| ------------ | -------- | -------- | ------- | ------------------------------- |
| `objectType` | `Q`      | ✅       | -       | The OSDK object type to display |
| `className`  | `string` | ❌       | -       | CSS class for custom styling    |
| `rowHeight`  | `number` | ❌       | `40`    | Height of each row in pixels    |

### Column Management

| Prop                        | Type                           | Default | Description                                               |
| --------------------------- | ------------------------------ | ------- | --------------------------------------------------------- |
| `columnDefinitions`         | `Array<ColumnDefinition>`      | -       | Ordered list of columns. If omitted, shows all properties |
| `onColumnVisibilityChanged` | `(newStates) => void`          | -       | Called when column visibility changes                     |
| `onColumnsPinnedChanged`    | `(newStates) => void`          | -       | Called when column pinning changes                        |
| `onColumnResize`            | `(columnId, newWidth) => void` | -       | Called when a column is resized                           |

### Filtering

> **Note:** The table filtering UI is not yet supported. However, you can still pass a `filter` prop to programmatically filter the objects displayed in the table.

| Prop              | Type                   | Default | Description                                                      |
| ----------------- | ---------------------- | ------- | ---------------------------------------------------------------- |
| `enableFiltering` | `boolean`              | `true`  | Whether filtering menu items are shown in the column header menu |
| `filter`          | `WhereClause<Q, RDPs>` | -       | Current where clause filter (controlled mode)                    |
| `onFilterChanged` | `(newWhere) => void`   | -       | Required when `filter` is provided                               |

### Sorting

| Prop               | Type                           | Default | Description                          |
| ------------------ | ------------------------------ | ------- | ------------------------------------ |
| `enableOrdering`   | `boolean`                      | `true`  | Whether sorting menu items are shown |
| `defaultOrderBy`   | `Array<{property, direction}>` | -       | Initial sort order (uncontrolled)    |
| `orderBy`          | `Array<{property, direction}>` | -       | Current sort order (controlled)      |
| `onOrderByChanged` | `(newOrderBy) => void`         | -       | Required when `orderBy` is provided  |

### Column Features

| Prop                   | Type      | Default | Description                                     |
| ---------------------- | --------- | ------- | ----------------------------------------------- |
| `enableOrdering`       | `boolean` | `true`  | Whether sorting menu items are shown            |
| `enableColumnPinning`  | `boolean` | `true`  | Whether pinning menu items are shown            |
| `enableColumnResizing` | `boolean` | `true`  | Whether resize menu item is shown               |
| `enableColumnConfig`   | `boolean` | `true`  | Whether column configuration menu item is shown |

#### Hiding Header Menu Items

Each column header has a menu with items for sorting, filtering, pinning, resizing, and column configuration. You can hide specific menu items by setting the corresponding `enable...` prop to `false`:

```typescript
<ObjectTable
  objectType={Employee}
  enableFiltering={false} // Hides "Filter" menu items from column headers
  enableOrdering={false} // Hides "Sort" menu items from column headers
  enableColumnPinning={false} // Hides "Pin" menu items from column headers
  enableColumnResizing={false} // Hides "Resize" menu item from column headers
  enableColumnConfig={false} // Hides "Column configuration" menu item from column headers
/>;
```

### Row Selection

| Prop             | Type                                     | Default  | Description                                            |
| ---------------- | ---------------------------------------- | -------- | ------------------------------------------------------ |
| `selectionMode`  | `"single" \| "multiple" \| "none"`       | `"none"` | Selection mode. "multiple" shows checkboxes            |
| `selectedRows`   | `PrimaryKeyType<Q>[]`                    | -        | Selected rows (controlled mode)                        |
| `isAllSelected`  | `boolean`                                | -        | Indicates all rows are selected (controlled mode only) |
| `onRowSelection` | `(selectedRowIds, isSelectAll?) => void` | -        | Required when `selectedRows` is provided               |

### Interactions

| Prop                    | Type                            | Description                                  |
| ----------------------- | ------------------------------- | -------------------------------------------- |
| `onRowClick`            | `(object) => void`              | Called when a row is clicked                 |
| `renderCellContextMenu` | `(row, cellValue) => ReactNode` | Custom context menu for right-click on cells |

### Cell Editing

> The editable feature allows inline editing with validation and bulk submission capabilities. Editable cells support text inputs, number inputs, and dropdown selectors.

| Prop                 | Type                                          | Description                                                                                                                     |
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `editMode`           | `"always" \| "manual"`                        | Controls edit mode behavior. "always": Table is always in edit mode. "manual": User toggles edit mode on/off. Default: "manual" |
| `onCellValueChanged` | `(info: CellEditInfo) => void`                | Called when a cell value is edited. The info object contains rowId, columnId, newValue, oldValue, and originalRowData           |
| `onSubmitEdits`      | `(edits: CellEditInfo[]) => Promise<boolean>` | When provided, shows a "Submit Edits" button that calls this function with all pending edits. Return true on success            |

## Column Definitions

### Column Definition Structure

```typescript
type ColumnDefinition<Q, RDPs, FunctionColumns> = {
  locator: ColumnDefinitionLocator<Q, RDPs, FunctionColumns>;
  isVisible?: boolean; // default: true
  pinned?: "left" | "right" | "none"; // default: "none"
  width?: number; // Fixed width in pixels
  minWidth?: number; // Minimum width
  maxWidth?: number; // Maximum width
  resizable?: boolean; // Allow column resizing
  orderable?: boolean; // Allow column sorting
  filterable?: boolean; // Allow column filtering
  editable?: boolean; // Allow inline editing for this column
  editFieldConfig?: EditFieldConfig; // Optional editor component config (e.g. dropdown)
  validateEdit?: (value: unknown) => Promise<string | undefined>; // Custom validation function for cell edits
  renderCell?: (object, locator) => React.ReactNode; // Custom cell renderer
  columnName?: string; // Custom column name for the header
  renderHeader?: () => React.ReactNode; // Custom header renderer (takes precedence over columnName)
};
```

#### `editFieldConfig`

When `editable` is `true`, columns default to a text or number input (auto-detected from the property type). Use `editFieldConfig` to specify a different editor component.

**Supported editor components:**

| `fieldComponent` | Description                              | Renders                                                      |
| ---------------- | ---------------------------------------- | ------------------------------------------------------------ |
| `"DROPDOWN"`     | A select dropdown or searchable combobox | `Select` (default) or `Combobox` (when `isSearchable: true`) |

Without `editFieldConfig`, editable columns use a text input for string properties and a number input for numeric properties (`double`, `integer`, `long`, `float`, `decimal`, `byte`, `short`).

**Dropdown `fieldComponentProps`:**

| Prop                | Type                      | Default        | Description                                              |
| ------------------- | ------------------------- | -------------- | -------------------------------------------------------- |
| `items`             | `V[]`                     | (required)     | Available items for the dropdown                         |
| `isSearchable`      | `boolean`                 | `false`        | Renders a searchable combobox instead of a select        |
| `placeholder`       | `string`                  | -              | Placeholder text when no value is selected               |
| `itemToStringLabel` | `(item: V) => string`     | `String(item)` | Converts an item to a display string                     |
| `itemToKey`         | `(item: V) => string`     | -              | Returns a unique key for each item (used as React `key`) |
| `isItemEqual`       | `(a: V, b: V) => boolean` | `Object.is`    | Custom equality check (required when items are objects)  |
| `isMultiple`        | `boolean`                 | `false`        | Whether multiple values can be selected                  |

#### `columnName` vs `renderHeader`

- **`columnName`**: If provided, this string is used as the column header text. If not provided, property columns default to the property's `displayName`, and other column types default to the `id`.
- **`renderHeader`**: If provided, this function renders the header component. When both `columnName` and `renderHeader` are provided, `renderHeader` takes precedence in the table header, but `columnName` is still used in other places where the column name is displayed (e.g., the column configuration dialog, multi-sort dialog).

### Column Locator Types

#### 1. Property Column

Displays a property from the object type:

```typescript
{
  type: "property",
  id: "propertyName"  // Must be a valid property key
}
```

#### 2. Derived Property (RDP) Column

Displays a computed property:

```typescript
{
  type: "rdp",
  id: "customPropertyName",
  creator: DerivedProperty.creator(/* ... */)
}
```

#### 3. Function Column

Displays values computed by an OSDK function (query). This is equivalent to Workshop's [function-backed columns](https://www.palantir.com/docs/foundry/workshop/widgets-object-table/#function-backed-columns). The function must accept an ObjectSet parameter and return a map of results keyed per object.

```typescript
{
  type: "function",
  id: "columnKey",                // Key in your FunctionColumns type map
  queryDefinition: myQuery,       // The OSDK query definition to execute
  getFunctionParams: (objectSet) => ({ objectSetKey: objectSet }),
  getKey: (object) => `${object.$objectType}:${object.$primaryKey}`, // The key to index the value of an object
  getValue: (cellData) => cellData?.status,  // Getter to extract the value from the raw data
  dedupeIntervalMs: 5 * 60 * 1_000, // The stale time of your data, if multiple requests happen within this interval, no new network call will be made
}
```

| Property            | Type                     | Required | Description                                                                                                                                                                   |
| ------------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`              | `"function"`             | Yes      | Identifies this as a function column                                                                                                                                          |
| `id`                | `keyof FunctionColumns`  | Yes      | Key in the FunctionColumns type map                                                                                                                                           |
| `queryDefinition`   | `QueryDefinition`        | Yes      | The OSDK query definition to execute                                                                                                                                          |
| `getFunctionParams` | `(objectSet) => params`  | Yes      | Computes function parameters from the current ObjectSet                                                                                                                       |
| `getKey`            | `(object) => string`     | Yes      | Generates a lookup key for each object in the result map                                                                                                                      |
| `getValue`          | `(cellData?) => unknown` | No       | Extracts a display value from the raw function result per object. `cellData` is `undefined` when the object has no result (e.g., loading or missing from the function output) |
| `dedupeIntervalMs`  | `number`                 | No       | Minimum time (ms) between re-fetches of the same function with the same parameters. Defaults to `300_000` (5 minutes)                                                         |

#### 4. Custom Column

Displays header and cell with the provided custom renderers.

```typescript
{
  type: "custom",
  id: "columnName" 
}
```

## Examples

### Example 1: Basic Table with Custom Column Definitions

```typescript
import {
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    pinned: "left",
    width: 200,
  },
  {
    locator: { type: "property", id: "email" },
    width: 250,
  },
  {
    locator: { type: "property", id: "jobTitle" },
  },
  {
    locator: { type: "property", id: "department" },
  },
];

function EmployeesTable() {
  return (
    <ObjectTable
      objectType={Employee}
      columnDefinitions={columnDefinitions}
    />
  );
}
```

### Example 2: Table with Multiple Selection

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

function EmployeesTable() {
  return (
    <ObjectTable
      objectType={Employee}
      selectionMode="multiple"
    />
  );
}
```

### Example 3: Table with Default Sorting

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

function EmployeesTable() {
  return (
    <ObjectTable
      objectType={Employee}
      defaultOrderBy={[{
        property: "firstFullTimeStartDate",
        direction: "desc",
      }]}
    />
  );
}
```

### Example 4: Custom Cell Rendering

```typescript
import {
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    renderCell: (employee) => (
      <strong style={{ color: "blue" }}>
        {employee.fullName}
      </strong>
    ),
  },
  {
    locator: { type: "property", id: "firstFullTimeStartDate" },
    renderCell: (employee) => {
      const date = employee.firstFullTimeStartDate;
      return date ? new Date(date).toLocaleDateString() : "-";
    },
  },
];

function EmployeesTable() {
  return (
    <ObjectTable
      objectType={Employee}
      columnDefinitions={columnDefinitions}
    />
  );
}
```

### Example 5: Custom Header Rendering

```typescript
import {
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    renderHeader: () => (
      <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
        <span>👤</span>
        <span>Employee Name</span>
      </div>
    ),
  },
];

function EmployeesTable() {
  return (
    <ObjectTable
      objectType={Employee}
      columnDefinitions={columnDefinitions}
    />
  );
}
```

### Example 6: Hidden Columns

Use `isVisible: false` to define columns that are hidden by default but can be toggled visible by the user:

```typescript
const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
  },
  {
    locator: { type: "property", id: "email" },
  },
  {
    locator: { type: "property", id: "jobTitle" },
    isVisible: false, // Hidden by default
  },
];
```

### Example 7: Context Menu on Cell Right-Click

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

function EmployeesTable() {
  const renderCellContextMenu = (employee: Employee, cellValue: unknown) => (
    <div
      style={{
        background: "white",
        border: "1px solid #ccc",
        borderRadius: "4px",
        padding: "8px",
      }}
    >
      <div onClick={() => console.log("View", employee.fullName)}>
        View Details
      </div>
      <div onClick={() => console.log("Edit", employee.fullName)}>
        Edit Employee
      </div>
      <div onClick={() => navigator.clipboard.writeText(String(cellValue))}>
        Copy Value
      </div>
    </div>
  );

  return (
    <ObjectTable
      objectType={Employee}
      renderCellContextMenu={renderCellContextMenu}
    />
  );
}
```

### Example 8: Row Click Handler

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";
import { useRouter } from "next/router";

function EmployeesTable() {
  const router = useRouter();

  const handleRowClick = (employee: Employee) => {
    router.push(`/employees/${employee.$primaryKey}`);
  };

  return (
    <ObjectTable
      objectType={Employee}
      onRowClick={handleRowClick}
    />
  );
}
```

### Example 9: Filtering on Object Properties and Derived Properties (RDPs)

You can filter by object properties and derived properties by including them in the `WhereClause`:

```typescript
import { DerivedProperty } from "@osdk/client";
import {
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

type RDPs = {
  managerName: string | undefined;
};

const columnDefinitions: Array<ColumnDefinition<typeof Employee, RDPs>> = [
  {
    locator: { type: "property", id: "fullName" },
  },
  {
    locator: {
      type: "rdp",
      id: "managerName",
      creator: DerivedProperty.creator<typeof Employee, string | undefined>(
        (base) =>
          base.lead.select({
            fullName: true,
          }),
        (pivot) => pivot?.fullName,
      ),
    },
    renderHeader: () => <span>Manager</span>,
  },
];

function EmployeesWithManagerTable() {
  return (
    <ObjectTable
      objectType={Employee}
      columnDefinitions={columnDefinitions}
      filter={{
        fullName: { $containsAnyTerm: "Paul" },
        managerName: { $eq: "Jane Smith" },
      }}
    />
  );
}
```

### Example 10: Controlled Sorting

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";
import { useState } from "react";

function EmployeesTable() {
  const [orderBy, setOrderBy] = useState<
    Array<{
      property: keyof Employee;
      direction: "asc" | "desc";
    }>
  >([
    { property: "fullName", direction: "asc" },
  ]);

  return (
    <ObjectTable
      objectType={Employee}
      orderBy={orderBy}
      onOrderByChanged={setOrderBy}
    />
  );
}
```

### Example 11: Controlled Row Selection

```typescript
import { ObjectTable } from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";
import { useState } from "react";

function EmployeesTable() {
  const [selectedRows, setSelectedRows] = useState<string[]>([]);
  const [isAllSelected, setIsAllSelected] = useState(false);

  const handleRowSelection = (
    selectedRowIds: string[],
    isSelectAll?: boolean,
  ) => {
    if (isSelectAll) {
      if (selectedRowIds.length === 0) {
        setIsAllSelected(false);
        setSelectedRows([]);
      } else {
        setIsAllSelected(true);
        setSelectedRows([]);
      }
    } else {
      setIsAllSelected(false);
      setSelectedRows(selectedRowIds);
    }
  };

  return (
    <div>
      <div>Selected: {selectedRows.length} employees</div>
      <ObjectTable
        objectType={Employee}
        selectionMode="multiple"
        selectedRows={selectedRows}
        isAllSelected={isAllSelected}
        onRowSelection={handleRowSelection}
      />
    </div>
  );
}
```

**Key points about select all behavior:**

- The `isSelectAll` parameter in `onRowSelection` indicates whether the change was triggered by the "select all" checkbox
- When `isAllSelected` is `true`, the table shows all rows as selected regardless of the `selectedRows` array content
- This allows efficient handling of "select all" without loading all object IDs
- Individual row selections automatically set `isAllSelected` to `false`

### Example 12: Custom Column Type

In a custom column type, you can render anything in the column by passing in renderHeader and renderCell props.

```typescript
import {
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: {
      type: "custom",
      id: "Custom Column",
    },
    renderHeader: () => "Custom",
    renderCell: (object: Osdk.Instance<Employee>) => {
      return (
        <button onClick={() => alert(`Clicked ${object["$title"]}`)}>
          Click me
        </button>
      );
    },
    orderable: false,
  },
];

function EmployeesTable() {
  return (
    <ObjectTable
      objectType={Employee}
      columnDefinitions={columnDefinitions}
    />
  );
}
```

### Example 13: Editable Table

Enable inline editing with validation, dropdown selectors, and bulk submission:

```typescript
import { useOsdkAction } from "@osdk/react";
import {
  type CellEditInfo,
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee, updateMultipleEmployees } from "@YourApp/sdk";

const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    editable: true, // Default text input
  },
  {
    locator: { type: "property", id: "email" },
    editable: true,
    validateEdit: async (value) => {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(value as string)
        ? undefined
        : "Please enter a valid email address";
    },
  },
  {
    locator: { type: "property", id: "department" },
    editable: true,
    editFieldConfig: {
      fieldComponent: "DROPDOWN",
      fieldComponentProps: {
        items: [
          "Engineering",
          "Product",
          "Design",
          "Sales",
          "Marketing",
          "Finance",
          "Human Resources",
        ],
      },
    },
  },
  {
    locator: { type: "property", id: "jobTitle" },
    editable: true,
    editFieldConfig: {
      fieldComponent: "DROPDOWN",
      fieldComponentProps: {
        items: [
          "Software Engineer",
          "Senior Software Engineer",
          "Staff Engineer",
          "Engineering Manager",
          "Product Manager",
          "Designer",
        ],
        isSearchable: true, // Renders a searchable combobox
        placeholder: "Search job titles…",
      },
    },
  },
];

function EditableEmployeesTable() {
  const { applyAction } = useOsdkAction(updateMultipleEmployees);

  const handleCellValueChanged = (
    info: CellEditInfo<Employee>,
  ) => {
    console.log("Cell edited:", {
      rowId: info.rowId,
      columnId: info.columnId,
      oldValue: info.oldValue,
      newValue: info.newValue,
      originalRowData: info.originalRowData,
    });
  };

  // When onSubmitEdits is provided, a "Submit Edits" button appears in the table
  const handleSubmitEdits = async (edits: CellEditInfo<Employee>[]) => {
    try {
      // Transform edits array into format expected by your action
      const updates = edits.map(edit => ({
        employeeId: edit.rowId,
        field: edit.columnId,
        value: edit.newValue,
      }));

      await applyAction({ updates });

      // Return true to indicate successful submission
      return true;
    } catch (error) {
      console.error("Failed to save edits:", error);
      // Return false or throw to indicate failure
      return false;
    }
  };

  return (
    <ObjectTable
      objectType={Employee}
      columnDefinitions={columnDefinitions}
      editMode="manual" // User can toggle edit mode on/off
      onCellValueChanged={handleCellValueChanged}
      onSubmitEdits={handleSubmitEdits} // Shows "Submit Edits" button
    />
  );
}
```

#### Key features of editable tables:

1. **Edit Modes**:
   - `manual` (default): User clicks "Edit Table" button to enter edit mode
   - `always`: Table is always in edit mode

2. **Editor Components**:
   - **Text input** (default): For string properties
   - **Number input** (auto-detected): For numeric properties (`double`, `integer`, `long`, `float`, `decimal`, `byte`, `short`)
   - **Dropdown (Select)**: Fixed list of options via `editFieldConfig` with `fieldComponent: "DROPDOWN"`
   - **Dropdown (Combobox)**: Searchable list via `isSearchable: true` in `fieldComponentProps`

3. **Validation**:
   - Use `validateEdit` on columns for async validation
   - Validation errors are shown with an error icon and tooltip
   - Works with all editor types including dropdowns

4. **Edit State Management**:
   - Edits are tracked locally until submitted
   - Modified cells are visually highlighted
   - "Cancel" button discards all pending edits

5. **Bulk Submission**:
   - When `onSubmitEdits` is provided, a "Submit Edits" button appears
   - All edits are submitted together
   - Return `true` from `onSubmitEdits` to clear edits after successful submission

### Example 14: Custom Column Configuration Dialog

Use the `ColumnConfigDialog` component to create a custom column configuration experience:

```typescript
import {
  ColumnConfigDialog,
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";
import { useCallback, useMemo, useState } from "react";

const initialColumnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    columnName: "Full Name",
  },
  {
    locator: { type: "property", id: "emailPrimaryWork" },
    columnName: "Email",
  },
  {
    locator: { type: "property", id: "jobTitle" },
    columnName: "Job Title",
  },
  {
    locator: { type: "property", id: "department" },
    columnName: "Department",
  },
  {
    locator: { type: "property", id: "businessTitle" },
    columnName: "Business Title",
    isVisible: false, // Hidden by default
  },
];

function EmployeesTable() {
  const [isColumnConfigOpen, setIsColumnConfigOpen] = useState(false);
  const [columnDefinitions, setColumnDefinitions] = useState(
    initialColumnDefinitions,
  );

  // Build column options for the dialog
  const columnOptions = useMemo(
    () =>
      initialColumnDefinitions.map((colDef) => ({
        id: colDef.locator.id,
        name: colDef.columnName || colDef.locator.id,
      })),
    [],
  );

  // Track current visibility state
  const currentVisibility = useMemo(() => {
    const visibility: Record<string, boolean> = {};
    initialColumnDefinitions.forEach((colDef) => {
      visibility[colDef.locator.id] = columnDefinitions.some(
        (def) => def.locator.id === colDef.locator.id,
      );
    });
    return visibility;
  }, [columnDefinitions]);

  // Track current column order
  const currentColumnOrder = useMemo(
    () => columnDefinitions.map((colDef) => colDef.locator.id),
    [columnDefinitions],
  );

  const handleApplyColumnConfig = useCallback(
    (columns: Array<{ columnId: string; isVisible: boolean }>) => {
      const newColumnDefinitions: Array<ColumnDefinition<typeof Employee>> = [];

      // Apply the new visibility and order
      columns.forEach(({ columnId, isVisible }) => {
        if (isVisible) {
          const colDef = initialColumnDefinitions.find(
            (def) => def.locator.id === columnId,
          );
          if (colDef) {
            newColumnDefinitions.push(colDef);
          }
        }
      });

      setColumnDefinitions(newColumnDefinitions);
      setIsColumnConfigOpen(false);
    },
    [],
  );

  return (
    <>
      <div style={{ marginBottom: "16px" }}>
        <button
          onClick={() => setIsColumnConfigOpen(true)}
        >
          Configure Columns
        </button>
      </div>

      <ObjectTable
        objectType={Employee}
        columnDefinitions={columnDefinitions}
        enableColumnConfig={false} // Disable built-in config since we're using custom
      />

      <ColumnConfigDialog
        isOpen={isColumnConfigOpen}
        onClose={() => setIsColumnConfigOpen(false)}
        columnOptions={columnOptions}
        currentVisibility={currentVisibility}
        currentColumnOrder={currentColumnOrder}
        onApply={handleApplyColumnConfig}
      />
    </>
  );
}
```

This example demonstrates:

- Using the `ColumnConfigDialog` component for custom column management
- Tracking column visibility and order in component state
- Providing a custom button to open the dialog
- Disabling the built-in column configuration to avoid conflicts
- Managing hidden columns that can be toggled visible by users

### Example 15: Function-Backed Columns

Display values computed by OSDK functions (queries) alongside regular property columns. Function columns automatically handle loading states, caching, and deduplication.

```typescript
import {
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee, getEmployeeMetrics } from "@YourApp/sdk";

// Define a type map for your function columns
type EmployeeFunctionColumns = {
  metrics: typeof getEmployeeMetrics;
};

const columnDefinitions: Array<
  ColumnDefinition<
    typeof Employee,
    Record<string, never>,
    EmployeeFunctionColumns
  >
> = [
  {
    locator: { type: "property", id: "fullName" },
    pinned: "left",
    width: 200,
  },
  {
    locator: { type: "property", id: "department" },
  },
  {
    locator: {
      type: "function",
      id: "metrics",
      queryDefinition: getEmployeeMetrics,
      // Pass the current object set as a parameter to the function
      getFunctionParams: (objectSet) => ({ employees: objectSet }),
      // Generate a unique key for each object to look up its result
      getKey: (employee) => `${employee.$objectType}:${employee.$primaryKey}`,
      // Extract the specific value to display from the function result
      getValue: (cellData) =>
        (cellData as { score: number } | undefined)?.score,
      // Cache results for 1 minute instead of the default 5
      dedupeIntervalMs: 60_000,
    },
    columnName: "Performance Score",
  },
];

function EmployeesWithMetricsTable() {
  return (
    <ObjectTable
      objectType={Employee}
      columnDefinitions={columnDefinitions}
    />
  );
}
```

Multiple function columns sharing the same `queryDefinition` are automatically deduplicated into a single API call. Use different `getValue` functions to extract different fields from the same result:

```typescript
const columnDefinitions: Array<
  ColumnDefinition<
    typeof Employee,
    Record<string, never>,
    EmployeeFunctionColumns
  >
> = [
  {
    locator: {
      type: "function",
      id: "metrics",
      queryDefinition: getEmployeeMetrics,
      getFunctionParams: (objectSet) => ({ employees: objectSet }),
      getKey: (emp) => `${emp.$objectType}:${emp.$primaryKey}`,
      getValue: (cellData) => (cellData as { score: number })?.score,
    },
    columnName: "Score",
  },
  {
    locator: {
      type: "function",
      id: "metrics",
      queryDefinition: getEmployeeMetrics,
      getFunctionParams: (objectSet) => ({ employees: objectSet }),
      getKey: (emp) => `${emp.$objectType}:${emp.$primaryKey}`,
      getValue: (cellData) => (cellData as { rank: string })?.rank,
    },
    columnName: "Rank",
  },
];
```

### Column Pinning

Pin columns to the left or right side of the table:

```typescript
const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    pinned: "left", // Stays visible when scrolling horizontally
  },
  {
    locator: { type: "property", id: "email" },
    pinned: "right",
  },
];
```

### Column Resizing

Control whether columns can be resized:

```typescript
const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    resizable: true,
    minWidth: 150,
    maxWidth: 500,
  },
];
```

Listen to resize events:

```typescript
<ObjectTable
  objectType={Employee}
  onColumnResize={(columnId, newWidth) => {
    console.log(`Column ${columnId} resized to ${newWidth}px`);
  }}
/>;
```

### Disable Filtering or Sorting

Disable filtering or sorting globally:

```typescript
<ObjectTable
  objectType={Employee}
  enableColumnPinning={false}
  enableColumnResizing={false}
  enableColumnConfig={false}
  enableOrdering={false}
/>;
```

Or per column:

```typescript
const columnDefinitions: Array<ColumnDefinition<typeof Employee>> = [
  {
    locator: { type: "property", id: "fullName" },
    orderable: false,
    filterable: false,
  },
];
```

### Custom Row Height

Adjust row height for better readability:

```typescript
<ObjectTable
  objectType={Employee}
  rowHeight={56} // Larger rows for more content
/>;
```

### Loading and Empty States

The ObjectTable automatically handles:

- **Loading state**: Shows skeleton rows while data is loading
- **Empty state**: Shows appropriate message when no data matches filters
- **Error state**: Displays error messages if data fetching fails

No additional configuration needed - these states are built-in!

### Infinite Scrolling

The ObjectTable automatically implements infinite scroll pagination, with page size of 50. As users scroll down, more data is loaded seamlessly. No configuration required!

## TypeScript Tips

### Type-Safe Column Definitions

Use TypeScript generics to ensure type safety:

```typescript
import {
  type ColumnDefinition,
  ObjectTable,
} from "@osdk/react-components/experimental/object-table";
import { Employee } from "@YourApp/sdk";

type RDPs = {
  managerName: string | undefined;
  yearsOfService: number;
};

const columnDefinitions: Array<ColumnDefinition<typeof Employee, RDPs>> = [
  // TypeScript will validate property names and types
  {
    locator: { type: "property", id: "fullName" }, // ✅ Valid
  },
  {
    locator: { type: "property", id: "invalidProp" }, // ❌ Type error
  },
];
```

### Inferring Types from Object Type

Let TypeScript infer types from your OSDK object type:

```typescript
import type { PropertyKeys } from "@osdk/client";
import { Employee } from "@YourApp/sdk";

// PropertyKeys gives you all valid property names
type EmployeeProps = PropertyKeys<typeof Employee>;
```

## Troubleshooting

### Table not displaying data

- Ensure your OSDK client is properly configured
- Check that the object type is imported correctly
- Verify network requests in browser DevTools

### Type errors with columnDefinitions

- Ensure you're using the correct type parameters: `ColumnDefinition<typeof YourObjectType, RDPs, FunctionColumns>`
- Property IDs must exactly match property names from your object type

### Selection not working

- Ensure `selectionMode` is set to "single" or "multiple"
- For controlled mode, provide both `selectedRows` and `onRowSelection`

### Custom rendering not appearing

- Ensure `renderCell` returns valid React elements
- Check browser console for errors in your render function

### Table has no styling or looks broken

- Ensure you've imported `@osdk/react-components/styles.css` in your main CSS file
- Check that the CSS import is in the correct location (application entry point)
- Check browser DevTools to confirm CSS custom properties are loaded

## Theming

The ObjectTable (and all OSDK components) can be themed using CSS custom properties included in `@osdk/react-components/styles.css`.

### Understanding Token Scopes

**OSDK Tokens (`--osdk-*`)**

- All tokens used in OSDK components are prefixed with `--osdk-`
- Any Blueprint token used in OSDK components is mapped to an `--osdk-*` token
- Override these to theme **OSDK components only**
- Safe to customize without affecting other Blueprint components in your app

**Blueprint Tokens (`--bp-*`)**

- Core design tokens from Blueprint design system
- Override these to theme **both Blueprint and OSDK components**
- Use this for consistent theming across your entire application

### Customization Strategies

#### 1. Override OSDK Tokens Only

Change OSDK component styling without affecting other Blueprint components in your app:

```css
@layer osdk.styles, user.theme;

@import "@osdk/react-components/styles.css" layer(osdk.styles);

@layer user.theme {
  :root {
    /* Only affects OSDK table headers */
    --osdk-table-header-bg: #f0f0f0;
    --osdk-table-border-color: #e0e0e0;
    --osdk-table-row-hover-bg: #f9fafb;

    /* Only affects OSDK components using primary intent */
    --osdk-intent-primary-rest: #2563eb;
    --osdk-intent-primary-hover: #1d4ed8;
  }
}
```

#### 2. Override Blueprint Tokens

Change both Blueprint and OSDK components for consistent theming:

```css
@layer osdk.styles, user.theme;

@import "@osdk/react-components/styles.css" layer(osdk.styles);

@layer user.theme {
  :root {
    /* Affects ALL components (Blueprint + OSDK) using primary intent */
    --bp-intent-primary-rest: #2563eb;
    --bp-intent-primary-hover: #1d4ed8;
    --bp-intent-primary-active: #1e40af;

    /* Affects all spacing and borders across the design system */
    --bp-surface-spacing: 8px;
    --bp-surface-border-radius: 8px;
  }
}
```

#### 3. Scoped Overrides for Specific Tables

Apply custom styles to specific ObjectTable instances using the `className` prop:

```typescript
// Component
<ObjectTable
  objectType={Employee}
  className="custom-employee-table"
/>;
```

```css
/* Styles */
.custom-employee-table {
  --osdk-table-header-bg: #1e40af;
  --osdk-table-header-text-color: white;
  --osdk-table-row-hover-bg: #dbeafe;
}
```

### Common Theming Examples

#### Dark Mode

```css
@layer user.theme {
  [data-theme="dark"] {
    --osdk-table-header-bg: #1f2937;
    --osdk-table-border-color: #374151;
    --osdk-table-row-hover-bg: #374151;
    --osdk-surface-bg: #111827;
    --osdk-text-primary: #f9fafb;
  }
}
```

#### Compact Table

```css
.compact-table {
  --osdk-surface-spacing: 4px;
  --osdk-table-cell-padding: 8px;
}
```

```typescript
<ObjectTable
  objectType={Employee}
  className="compact-table"
  rowHeight={32}
/>;
```

#### Custom Brand Colors

```css
@layer user.theme {
  :root {
    /* Use your brand's primary color */
    --bp-intent-primary-rest: #7c3aed;
    --bp-intent-primary-hover: #6d28d9;
    --bp-intent-primary-active: #5b21b6;
  }
}
```

### Available CSS Variables

For a complete reference of all available CSS tokens for theming, see:

- [CSS Variables Documentation](./CSSVariables.md)

### Accessibility Note

When overriding theme tokens, ensure your custom colors meet accessibility standards:

- **Color contrast ratios** (WCAG AA): 4.5:1 for normal text, 3:1 for large text
- **Readable text** on all background colors
- **Clear visual distinction** between interactive states (rest, hover, active, disabled)

The default tokens are designed to meet WCAG AA standards.

## Additional Resources

- [ObjectTable API Reference](https://github.com/palantir/osdk-ts/blob/main/packages/react-components/src/object-table/ObjectTableApi.ts)
- [ObjectTable Implementation](https://github.com/palantir/osdk-ts/blob/main/packages/react-components/src/object-table/ObjectTable.tsx)
- [PeopleApp Examples](https://github.com/palantir/osdk-ts/blob/main/packages/e2e.sandbox.peopleapp/src/app/employees/EmployeesTable.tsx)
- [@osdk/react Documentation](https://github.com/palantir/osdk-ts/blob/main/docs/react/getting-started.md)
