Components

Tables

Tables in @wayvo-ai/core are built on TanStack Table with automatic integration with Stores. They provide sorting, filtering, pagination, and reactive updates.

PageLayoutTemplate

PageLayoutTemplate provides a complete table solution:

import { PageLayoutTemplate } from '@wayvo-ai/core/ui';

<PageLayoutTemplate
  title="Entities"
  subTitle="Manage your entities"
  store={store}
  smartSearchColumns={smartSearchColumns}
  tableColumns={tableColumns}
  pageId="entity-page"
  itemId="entity"
  editForm={<EditForm store={store} />}
  getDefaultRow={() => ({})}
  addNewButtonText="Add Entity"
/>

PageLayoutTemplate props

Required: store, smartSearchColumns, tableColumns, pageId, itemId, title, subTitle, icon.

Optional (grouped by purpose):

  • Layout / filter bar: filterStartContent (ReactNode) — Renders before Smart Search in the filter bar. filterEndContent (ReactNode) — Renders after filters (e.g. extra checkboxes). toolbarContent (ReactNode) — Renders in toolbar next to "Add New". hideColumnsMenu (boolean, default false) — Hides the columns visibility menu. hideFilters (boolean, default false) — Hides the Smart Search filter bar. hidePagination (boolean, default false) — Hides table pagination.
  • Edit form: editForm (ReactNode). getDefaultRow () => NewRow<T>. addNewButtonText (string, default "Add New"). editFormLayout ('sheet' | 'popup', default 'sheet'). popupWidth (number) — Width of edit popup when editFormLayout="popup". popupHeight (number) — Height of edit popup when editFormLayout="popup". rowClickToEdit (boolean, default false) — Clicking a row opens the edit form. allowDelete (boolean) — Shows Delete button in edit form footer. handleSave ((onClose: () => void) => Promise<void>) — Custom save handler; receives onClose so you can close after save. onSaveSuccess (() => void) — Callback after successful save.
  • Table: defaultVisibleColumnOrder (StringKeyof<T>[]) — Initial visible columns and their order. loadingRows (number) — Skeleton row count while loading. disableHeaderFilters (boolean) — Disables header filters on table columns (can also come from app context).
  • Smart Search: searchOnBlur (boolean) — Run search on blur instead of on every change. updateFilters ((filters: FiltersType<T>) => FiltersType<T>) — Transform or merge filters before apply. stickyFilters ((keyof T)[]) — Attribute codes that are "sticky": read-only in Smart Search, not cleared by "Clear", and kept when merging. Use when the page is opened in a context (e.g. customerId when viewing projects under a customer): stickyFilters={customerId ? ['customerId'] : undefined}.

Table Columns

Critical: Understanding row.original

IMPORTANT: In Wayvo tables, row.original only contains the row id, NOT the full data object.

// ❌ WRONG - row.original only has { id: "..." }
cell: ({ row }) => {
  const name = row.original.name; // undefined!
  return <span>{name}</span>;
}

// ✅ CORRECT - Use TableCell component
cell: (props) => <TableCell type="Text" attributeCode="name" {...props} />

// ✅ ALTERNATIVE - Use useRowValue hook
function NameCell({ store, rowId }: { store: Store<Entity>; rowId: string }) {
  const name = useRowValue(store, rowId, 'name');
  return <span>{name || '-'}</span>;
}

Column Definition Pattern

// hooks/use-table-columns.tsx
import type { Store } from '@wayvo-ai/core/common';
import type { Entity } from '@/lib/common/ds/types/module/Entity';
import type { AccessorKeyColumnDef } from '@tanstack/react-table';
import { HeaderCell, TableCell } from '@wayvo-ai/core/ui';
import { useMemo } from 'react';

export default function useTableColumns(store: Store<Entity>): AccessorKeyColumnDef<Entity>[] {
  return useMemo(
    () => [
      {
        accessorKey: 'name',
        header: (props) => <HeaderCell {...props} type="Text" store={store} accessorKey="name" title="Name" />,
        cell: (props) => <TableCell type="Text" attributeCode="name" {...props} />,
      },
      {
        accessorKey: 'status',
        header: (props) => <HeaderCell {...props} type="Text" store={store} accessorKey="status" title="Status" />,
        cell: (props) => <StatusBadgeCell attributeCode="status" {...props} />,
      },
    ],
    [store],
  );
}

Styled Cell Components

Pre-built styled cells are available:

import {
  EntityNameCell,
  StatusBadgeCell,
  CodeCell,
  BadgeOutlineCell,
  NumericWithUnitsCell,
  PercentageCell,
  CompoundCell,
} from '@wayvo-ai/core/ui';

// Entity name with icon
cell: (props) => <EntityNameCell attributeCode="name" preset="customer" useTableOnEdit {...props} />

// Status with auto-colors
cell: (props) => <StatusBadgeCell attributeCode="status" {...props} />

// Monospace code/ID
cell: (props) => <CodeCell attributeCode="taxId" {...props} />

// Short values (currency)
cell: (props) => <BadgeOutlineCell attributeCode="currency" {...props} />

// Number with unit
cell: (props) => <NumericWithUnitsCell attributeCode="amount" unit="$" {...props} />

// Percentage
cell: (props) => <PercentageCell attributeCode="allocationPercent" {...props} />

// Primary + secondary text
cell: (props) => (
  <CompoundCell
    primary="name"
    secondary="description"
    preset="customer"
    {...props}
  />
)

Custom Cell Components

For custom rendering, use useRowValue:

function CustomCell({ store, rowId }: { store: Store<Entity>; rowId: string }) {
  const status = useRowValue(store, rowId, 'status');
  const amount = useRowValue(store, rowId, 'amount');
  
  if (status == null) {
    return <span className="text-muted-foreground text-xs">-</span>;
  }
  
  return (
    <div className="flex items-center gap-2">
      <Badge variant={status === 'active' ? 'default' : 'secondary'}>
        {status}
      </Badge>
      {amount && <span className="font-mono">${amount.toLocaleString()}</span>}
    </div>
  );
}

// Use in column definition
{
  accessorKey: 'status',
  cell: ({ row }) => <CustomCell store={store} rowId={row.id} />,
}

Entity Presets

EntityNameCell and CompoundCell support presets:

  • customer - Building2 icon, primary color
  • user - User icon, blue
  • project - FolderKanban icon, purple
  • vendor - Store icon, emerald
  • document - FileText icon, amber
  • task - CheckSquare icon, cyan

Row Click Handling

Use rowClickToEdit to open the edit form when the user clicks a row:

<PageLayoutTemplate
  store={store}
  tableColumns={tableColumns}
  rowClickToEdit={true}
  editForm={<EditForm store={store} />}
  // ... other props
/>

Example: Sticky filters and filter bar content

When the page is opened in a context (e.g. projects under a customer), pass stickyFilters so that context filter cannot be removed or cleared. Use filterEndContent for extra controls like "Include archived":

const customerId = searchParams.get('customerId');
const [includeArchived, setIncludeArchived] = useState(false);

useEffect(() => {
  if (customerId) {
    store.setSmartSearchFilters([{ customerId: { is: customerId } }]);
  }
  store.executeQuery();
}, [customerId, includeArchived, store]);

<PageLayoutTemplate
  // ... required props
  stickyFilters={customerId ? ['customerId'] : undefined}
  filterEndContent={
    <div className="flex items-center gap-2 pr-2">
      <Checkbox
        id="include-archived"
        checked={includeArchived}
        onCheckedChange={(checked) => setIncludeArchived(checked === true)}
      />
      <Label htmlFor="include-archived">Include archived</Label>
    </div>
  }
/>

Example: Custom save and close

Use handleSave when you need to run logic after save and then close the form yourself (e.g. refresh another row). The handler receives onClose; call it after a successful save:

const handleSave = useCallback(
  async (onClose: () => void) => {
    const success = await store.save({ feedback: 'Address saved successfully' });
    if (success) {
      // Optional: refresh related data, then close
      onClose();
    }
  },
  [store],
);

<PageLayoutTemplate
  // ... required props
  handleSave={handleSave}
/>

Use onSaveSuccess when you only need a callback after save (e.g. invalidate queries); the form closes automatically:

const handleSaveSuccess = useCallback(() => {
  invalidateQuery('getCustomerDashboardStats');
}, []);

<PageLayoutTemplate
  // ... required props
  onSaveSuccess={handleSaveSuccess}
/>

Next Steps

Previous
Smart Search