Skip to content

Table Component Documentation

Waldur Homeport uses a unified table structure to display list data, grids, search queries, and custom filters.

To make table integration rules, properties, and layouts easier to navigate, the documentation is divided into 8 modular pillars:


Documentation Pillars

Pillar Description
Pillar 1: Getting Started Core import rules, Null/Empty wrappers (renderFieldOrDash), and a basic example.
Pillar 2: Data Fetching & Type Safety fetchData handlers, createFetcher (SDK functions), custom mock fetchers, createClientPaginatedFetcher (static lists), search query input (queryField), and type safety.
Pillar 3: Column Setup & Configuration Column parameters (width, alignment, copying, ellipsis, click blocking), sorting, optional columns, and key fetching.
Pillar 4: Row Actions & Selection 3-dots action menus (ActionsDropdown + ActionItem), row expansion (isRowExpandable), multi-select, and bulk toolbars.
Pillar 5: Filters & URL Sync OpenAPI generated filters, Redux caching, syncing to URL query formatting (uuid::name), and cell inline filtering.
Pillar 6: Visual Customizations Grid layouts (gridItem + gridSize + gridSpace), custom empty placeholders, header titles, styling class triggers, and standalone mode.
Pillar 7: Layout Wrappers & Portals Rendering multiple tables within tabs (TableWithTabs) and forwarding rendering portals (portal).
Pillar 8: Form Integration Embedding tables as input fields in react-final-form (fieldType, fieldName, rowValidate).

Complex Table Example

Below is a complete, real-world example demonstrating how multiple advanced features (automatic titles, filters, subtabs, expandable rows, and optional columns) are combined:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import { useMemo } from 'react';
import Table from '@/table/Table';
import { useTable } from '@/table/useTable';
import { createFetcher } from '@/table/api';
import { useFilterValues } from '@/table/useFilterValues';
import { renderFieldOrDash } from '@/table/utils';
import { Badge } from 'react-bootstrap';
import { translate } from '@/i18n';
import { ActionsDropdown } from '@/table/ActionsDropdown';
import { ActionItem } from '@/resource/actions/ActionItem';
import { TableTab } from '@/table/types';

// 1. Filter component
const ReviewerPoolFilter = () => (
  <SelectFilter
    title={translate('Status')}
    name="status"
    options={[
      { label: translate('Accepted'), value: 'accepted' },
      { label: translate('Pending'), value: 'pending' },
    ]}
  />
);

// 2. Tabs hook - defines sub-navigation within the table
const usePoolTabs = (): TableTab[] => {
  return useMemo(
    () => [
      {
        key: 'pool',
        title: translate('Pool'),
        params: { tab: 'reviewer-pool', pool_tab: 'pool' },
        default: true,
      },
      {
        key: 'assignments',
        title: translate('Assignments'),
        params: { tab: 'reviewer-pool', pool_tab: 'assignments' },
      },
    ],
    [],
  );
};

// 3. Expandable row component
const ExpandableRow = ({ row }) => (
  <div className="p-4">
    <h6>{row.reviewer_name}</h6>
    <p>Email: {row.reviewer_email}</p>
    <p>Expertise: {row.expertise_areas?.join(', ')}</p>
  </div>
);

// 4. Main Section Component
export const ReviewerPoolSection = ({ call }) => {
  const values = useFilterValues('ReviewerPool');
  const tabs = usePoolTabs();

  // ALWAYS memoize filter queries to prevent infinite re-render loops!
  const filter = useMemo(
    () => ({
      call_uuid: call.uuid,
      invitation_status: values.status?.value,
    }),
    [call.uuid, values.status],
  );

  const tableProps = useTable({
    table: 'ReviewerPool',
    fetchData: createFetcher(reviewerPoolList),
    filter,
    syncFiltersToURL: true,
  });

  const columns = useMemo(
    () => [
      {
        id: 'reviewer',
        title: translate('Reviewer'),
        render: ({ row }) => (
          <div>
            <div className="fw-bold">{row.reviewer_name}</div>
            <small className="text-muted">{row.reviewer_email}</small>
          </div>
        ),
        keys: ['reviewer_name', 'reviewer_email'],
      },
      {
        id: 'status',
        title: translate('Status'),
        render: ({ row }) => (
          <Badge variant={row.status === 'accepted' ? 'success' : 'warning'}>
            {row.status_display}
          </Badge>
        ),
        keys: ['status', 'status_display'],
      },
      {
        id: 'assignments',
        title: translate('Assignments'),
        render: ({ row }) => `${row.current} / ${row.max}`,
        keys: ['current_assignments', 'max_assignments'],
        optional: true,
      },
    ],
    [],
  );

  return (
    <Table
      {...tableProps}
      columns={columns}
      title={translate('Reviewer pool')}
      tabs={tabs}
      verboseName={translate('reviewers')}
      hasQuery
      hasOptionalColumns
      showPageSizeSelector
      filters={<ReviewerPoolFilter />}
      expandableRow={ExpandableRow}
      tableActions={
        <BaseButton
          variant="primary"
          size="sm"
          onClick={handleInvite}
          label={translate('Invite reviewer')}
        />
      }
    />
  );
};