npm package: @izumisy-tailor/tailor-data-viewer
Description: Flexible data viewer component for Tailor Platform
Latest version: 0.3.4

# Tailor Data Viewer

A low-level React component library for building data table interfaces with Tailor Platform (GraphQL) backends. Provides composable hooks and compound components for query parameter management, table rendering, filtering, sorting, and pagination.

## Features

- **Separation of Concerns**: Data fetching, query parameter management, and UI are fully decoupled
- **`useCollectionVariables` Hook**: Manages filter, sort, and pagination state; outputs Tailor Platform-compatible GraphQL variables
- **`CollectionControlProvider`**: Shares collection control state via React Context across sibling components
- **`Table.*` Compound Components**: Static, unstyled table primitives (`<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`)
- **`DataTable.*` Compound Components**: Data-bound table with sort indicators, cell renderers, and `useDataTable` integration
- **`useDataTable` Hook**: Integrates data, column visibility, row operations (optimistic updates), and props generators
- **Column Definition Helper**: `createColumnHelper<TRow>()` returns `{ column, inferColumns }` with the row type bound
- **Metadata-based Inference**: `inferColumns()` auto-derives sort/filter config from generated table metadata
- **Utility Components**: `ColumnSelector`, `CsvButton`, `SearchFilterForm`, `Pagination` — all props-based, spreadable from hooks
- **Multi-sort Support**: Multiple simultaneous sort fields
- **Optimistic Updates**: `updateRow`, `deleteRow`, `insertRow` with rollback
- **Presentation Agnostic**: Same `useCollectionVariables` can drive tables, kanbans, calendars, etc.

## Installation

```bash
npm install @izumisy-tailor/tailor-data-viewer
# or
pnpm add @izumisy-tailor/tailor-data-viewer
# or
yarn add @izumisy-tailor/tailor-data-viewer
```

### For AI Users

If you're using AI coding assistants (Claude Code, Cursor, GitHub Copilot, etc.), we strongly recommend installing the Data Viewer skill:

```bash
npx skills add git@github.com:tailor-sandbox/tailor-data-viewer.git
```

This provides AI-optimized documentation for better code generation and assistance.

### Peer Dependencies

```bash
npm install react react-dom
```

## Quick Start

```tsx
import {
  useCollectionVariables,
  useDataTable,
  DataTable,
  Pagination,
  createColumnHelper,
} from "@izumisy-tailor/tailor-data-viewer/component";
import "@izumisy-tailor/tailor-data-viewer/styles/theme.css";

// 1. Define columns
const { column } = createColumnHelper<Order>();

const columns = [
  column({
    label: "Name",
    render: (row) => row.name,
    accessor: (row) => row.name,
    sort: { field: "name", type: "string" },
    filter: { field: "name", type: "string" },
  }),
  column({
    label: "Amount",
    render: (row) => String(row.amount),
    accessor: (row) => row.amount,
    sort: { field: "amount", type: "number" },
    filter: { field: "amount", type: "number" },
  }),
  column({
    label: "Status",
    render: (row) => row.status,
    filter: {
      field: "status",
      type: "enum",
      options: [
        { value: "DRAFT", label: "Draft" },
        { value: "APPROVED", label: "Approved" },
      ],
    },
  }),
  column({
    id: "actions",
    label: "Actions",
    width: 50,
    render: (row) => <button onClick={() => handleEdit(row)}>Edit</button>,
  }),
];

// 2. Build a page
function OrdersPage() {
  const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
  const [result] = useQuery({
    query: GET_ORDERS,
    variables: {
      ...variables.pagination,
      query: variables.query,
      order: variables.order
    }
  });

  const table = useDataTable<Order>({
    columns,
    data: result.data?.orders,
    loading: result.fetching,
    control,
  });

  return (
    <DataTable.Provider value={table}>
      <DataTable.Root>
        <DataTable.Headers />
        <DataTable.Body />
      </DataTable.Root>
      <Pagination />
    </DataTable.Provider>
  );
}
```

## API Overview

### `useCollectionVariables(options)`

Manages filter, sort, and pagination state. Returns `variables` containing `query`, `order`, and `pagination` sub-properties in Tailor Platform-compatible format.

```tsx
const { variables, control } = useCollectionVariables({
  params: {
    pageSize: 20,
    initialSort: [{ field: "createdAt", direction: "Desc" }],
  },
});

// Use variables in your query
const [result] = useQuery({
  query: GET_ORDERS,
  variables: {
    ...variables.pagination,
    query: variables.query,
    order: variables.order,
  },
});

// Filter operations
control.addFilter("status", "eq", "ACTIVE");
control.setFilters([{ field: "status", operator: "eq", value: "ACTIVE" }]);
control.removeFilter("status");
control.clearFilters();

// Sort operations
control.setSort("createdAt", "Desc");
control.setSort("name", "Asc", true); // append for multi-sort
control.clearSort();

// Pagination
control.nextPage(endCursor);
control.prevPage(startCursor);
control.resetPage();

// Page info tracking
control.setPageInfo(pageInfo);
control.hasPrevPage;  // boolean
control.hasNextPage;  // boolean
```

### `DataTable.Provider` / `useDataTableContext()` / `useCollectionControl()`

`DataTable.Provider` wraps the table UI and provides both data table and collection control context. All utility components (`Pagination`, `ColumnSelector`, `CsvButton`, `SearchFilterForm`) read from this context — no prop spreading needed.

When `control` is passed to `useDataTable`, `DataTable.Provider` automatically wraps a `CollectionControlProvider` so child components can use `useCollectionControl()`.

```tsx
<DataTable.Provider value={table}>
  <StatusFilter />   {/* useCollectionControl() inside */}
  <DataTable.Root>
    <DataTable.Headers />
    <DataTable.Body />
  </DataTable.Root>
  <Pagination />
</DataTable.Provider>
```

For cases where you need `CollectionControlProvider` without `DataTable.Provider` (e.g., non-table UIs), you can use it standalone:

```tsx
<CollectionControlProvider value={control}>
  <StatusFilter />
  <CustomKanbanBoard />
</CollectionControlProvider>
```

### Column Definition Helper

#### `column(options)`

Defines a column with required `label` and `render`. Optionally supports `sort`, `filter`, `accessor`, `width`, and `id`.

```tsx
column<Order>({
  label: "Name",
  render: (row) => <strong>{row.name}</strong>,
  sort: { field: "name", type: "string" },
  filter: { field: "name", type: "string" },
})
```

### Table Metadata Generator

This library includes a metadata generator for [Tailor Platform SDK](https://www.npmjs.com/package/@tailor-platform/sdk) that produces type-safe table metadata with `as const` assertions. The generated metadata is used by `inferColumns()` for automatic sort/filter configuration.

1. Configure the generator in your `tailor.config.ts`:

```typescript
import { defineConfig, defineGenerators } from "@tailor-platform/sdk";
import { dataViewerMetadataGenerator } from "@izumisy-tailor/tailor-data-viewer/generator";

export const generators = defineGenerators(
  dataViewerMetadataGenerator({
    distPath: "src/generated/data-viewer-metadata.generated.ts",
  }),
);

export default defineConfig({
  name: "my-app",
  // ... your config
});
```

2. Run the generator:

```bash
tailor-sdk generate
```

### `inferColumns(tableMetadata)`

`column()` requires manually specifying `sort`/`filter` type configs and enum `options` for every column. `inferColumns()` eliminates this boilerplate by automatically deriving these from the generated table metadata. Based on each field's type (string, number, date, enum, etc.), the appropriate `SortConfig` / `FilterConfig` is set automatically, and enum fields get their options populated from the schema.

```tsx
import { createColumnHelper } from "@izumisy-tailor/tailor-data-viewer/component";
import { tableMetadata } from "./generated/data-viewer-metadata.generated";

const { column, inferColumns } = createColumnHelper<Task>();
const infer = inferColumns(tableMetadata.task);

const taskColumns = [
  column(infer("title")),                            // sort/filter auto-configured from metadata
  column(infer("status")),                           // enum options auto-derived
  column(infer("dueDate")),                          // date type auto-detected
  column(infer("priority", { sort: false })),        // override: disable sort
  column({
    id: "actions",
    label: "Actions",
    render: (row) => <ActionMenu row={row} />,
  }),
];
```

`inferColumns()` can be freely mixed with manual `column()` definitions. Use manual `column()` for fields not in the metadata or those requiring custom configuration, and `inferColumns()` for everything else.

### `useDataTable(options)`

Integrates data, column visibility, row operations, and props generators.

```tsx
const table = useDataTable<Order>({
  columns,
  data: result.data?.orders,   // CollectionResult<Order>
  loading: result.fetching,
  error: result.error,
  control,
  onClickRow: (row) => navigate(`/orders/${row.id}`),
  rowActions: [
    { id: "delete", label: "Delete", variant: "destructive", onClick: (row) => handleDelete(row.id) },
  ],
});

// Wrap with DataTable.Provider (utility components read from context)
<DataTable.Provider value={table}>
  <DataTable.Root>...</DataTable.Root>
  <ColumnSelector />
  <CsvButton filename="orders" />
  <Pagination />
</DataTable.Provider>

// Column visibility
table.toggleColumn("amount");
table.showAllColumns();
table.hideAllColumns();
table.isColumnVisible("amount");

// Optimistic updates
const { rollback } = table.updateRow(rowId, { status: "APPROVED" });
const { rollback, deletedRow } = table.deleteRow(rowId);
const { rollback } = table.insertRow(newRow);
```

### `Table.*` — Static Table Components

Low-level table primitives without data binding. Use for fully custom layouts or skeleton loading.

```tsx
<Table.Root>
  <Table.Headers>
    <Table.HeaderRow>
      <Table.HeaderCell>Name</Table.HeaderCell>
      <Table.HeaderCell>Status</Table.HeaderCell>
    </Table.HeaderRow>
  </Table.Headers>
  <Table.Body>
    <Table.Row>
      <Table.Cell>Order A</Table.Cell>
      <Table.Cell>Approved</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table.Root>
```

### `DataTable.*` — Data-bound Table Components

Pair with `useDataTable` for automatic header sorting, cell rendering, and row operations.

```tsx
// Basic usage (wrap with DataTable.Provider)
<DataTable.Provider value={table}>
  <DataTable.Root>
    <DataTable.Headers />
    <DataTable.Body />
  </DataTable.Root>
</DataTable.Provider>

// Custom row rendering
<DataTable.Provider value={table}>
  <DataTable.Root>
    <DataTable.Headers />
    <DataTable.Body>
      {table.rows.map((row) => (
        <DataTable.Row
          key={row.id}
          onClick={() => navigate(`/orders/${row.id}`)}
        >
          {table.visibleColumns.map((col) => (
            <DataTable.Cell key={col.id ?? col.label} />
          ))}
        </DataTable.Row>
      ))}
    </DataTable.Body>
  </DataTable.Root>
</DataTable.Provider>
```

### Utility Components

All utility components read from `DataTable.Provider` context.

| Component | Props | Description |
|-----------|-------|-------------|
| `Pagination` | (none) | Previous/Next page controls |
| `ColumnSelector` | (none) | Column visibility toggle UI |
| `CsvButton` | `filename?` | Export visible data as CSV |
| `SearchFilterForm` | `labels?`, `trigger?` | Multi-field filter form with operator selection |

All utility components read from `DataTable.Provider` context — no prop spreading needed:

```tsx
<DataTable.Provider value={table}>
  <SearchFilterForm />
  <ColumnSelector />
  <CsvButton filename="orders-export" />
  <DataTable.Root>
    <DataTable.Headers />
    <DataTable.Body />
  </DataTable.Root>
  <Pagination />
</DataTable.Provider>
```

### Optimistic Updates in Cell Renderers

Use `useDataTableContext()` inside `DataTable.Provider` to access row operations from custom renderers.

```tsx
function StatusEditor({ row }: { row: Order }) {
  const { updateRow } = useDataTableContext<Order>();
  const [updateOrder] = useMutation(UPDATE_ORDER);

  const handleChange = async (newStatus: string) => {
    const { rollback } = updateRow(row.id, { status: newStatus });
    try {
      await updateOrder({ id: row.id, input: { status: newStatus } });
    } catch (error) {
      rollback();
    }
  };

  return <StatusSelect value={row.status} onChange={handleChange} />;
}

// Use in column definition
column<Order>({
  label: "Status",
  render: (row) => <StatusEditor row={row} />,
})
```

### Relation Fields

Use `column()` with GraphQL query expansion to show relation data.

```tsx
// Include relations in your GraphQL query
const GET_MEMBERSHIPS = graphql(`
  query SupplierGroupMemberships {
    supplierGroupMemberships {
      edges {
        node {
          id
          supplier { companyName, contactName }
          addedBy { name }
          createdAt
        }
      }
    }
  }
`);

// Display relation fields with column()
const columns = [
  column<Membership>({
    label: "Company",
    render: (row) => row.supplier?.companyName ?? "-",
  }),
  column<Membership>({
    label: "Added By",
    render: (row) => row.addedBy?.name ?? "-",
  }),
  column<Membership>({
    label: "Created",
    render: (row) => row.createdAt,
    sort: { field: "createdAt", type: "date" },
  }),
];
```

## Styling

The library uses Tailwind CSS classes. Import the included theme or provide your own CSS variables:

```css
/* Option 1: Import included theme */
@import "@izumisy-tailor/tailor-data-viewer/styles/theme.css";

/* Option 2: Define your own CSS variables */
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  /* ... see theme.css for full list */
}
```

## Exports

| Entry Point | Description |
|-------------|-------------|
| `@izumisy-tailor/tailor-data-viewer/component` | Hooks, components, helpers, and types |
| `@izumisy-tailor/tailor-data-viewer/generator` | Metadata generator for Tailor SDK |
| `@izumisy-tailor/tailor-data-viewer/styles` | CSS theme file |

## Development

See the [Development Guide](./DEVELOPMENT.md) for setup and contribution instructions.

## License

MIT