# NestedTable

**Category:** Base Components/Collections/NestedTable

## Design

```tsx
import { NestedTable, useNestedTable } from '@wix/patterns';
```

---

### Description

The Nested Table component handles hierarchical data with multiple levels of nesting, allowing users to easily navigate through parent-child relationships. It is ideal for displaying information that requires multiple levels of nesting, such as a file system or a product catalog.

### Supported Features
The following collection features are supported also by the Nested Table component:
- [Empty State](./?path=/story/features-display-empty-states--collectionemptystate)
- Error State - See `renderError` prop
- Loading state
- Filters

### Integration Notes

#### Up to 5 Levels of Nesting

The UX guideline for this component suggests defining up to 5 levels of nesting. This limitation is put in place to enhance readability and prevent potential issues with indentation and spacing, ensuring that the data remains comprehensible even with deeper nesting.

#### Lazy and Eager Mode

The component's initial load logic detects the number of items in the dataset to determine whether to operate in "eager" or "lazy" mode.

- **Eager Mode:** When the dataset contains less than 1000 items, all data will be rendered in the expanded mode state by default. Users can immediately see the complete dataset without needing to trigger any additional requests to the server.
- **Lazy Mode:** If the dataset contains more than 1000 items, the Nested Table will render only the first 100 items of the first level in a collapsed state. When the user expands a collapsed item, the component will trigger a query request to the server to retrieve the child items for that specific parent.
  > The number 1000 can be configured via [useNestedTable.fetchAllThreshold](./?path=/story/base-components-collections-nestedtable--usenestedtable) option.

#### Supported data types and query endpoints

The Nested Table component offers flexibility in handling data retrieval from different APIs, supporting two distinct types of query APIs:

- **Single Entity API:** This type of API is designed to retrieve data from all levels of the Nested Table in one request.
  Each level will be configured using the same [useNestedTable.levels[].createCollection.collection.fetchData](./?path=/story/base-components-collections-nestedtable--usenestedtable) implementation, simplifying the setup process.
  To facilitate this configuration, Wix Patterns provides a helper function called [createNestedTableSingleEntityLevels](./?path=/story/base-components-collections-nestedtable--createnestedtablesingleentitylevels).
  Developers can use this helper function to define the data-fetching logic for each level of nesting, ensuring consistency and ease of implementation.
See the [Single Entity](./?path=/story/base-components-collections-nestedtable--nestedtable#Single_Entity) example for more details.

- **Various Entities API:**
  In contrast, the Nested Table also supports scenarios where each level requires data retrieval from different query endpoints.
  This approach allows developers to customize and fine-tune the data retrieval process for individual levels, accommodating diverse data structures and sources.
  By providing the flexibility to specify relevant query endpoints for each nesting level, developers can tailor the data-fetching behavior to their specific needs. 
See the [Various Entities](./?path=/story/base-components-collections-nestedtable--nestedtable#Various_Entities) example for more details.

#### Filters
Nested table supports filters for each level. The filters are passed to the `fetchData` function for all collections across all levels.
Define `filters` in the filters property of `useNestedTable` and pass them to `createCollection` for each level.
The `filters` prop of the NestedTable collection works the same way as in other collection components.

The server must handle the filters and return the relevant data. When a user applies a filter related to a specific level, the server should return:
1. All ancestor records from higher levels
2. The filtered records at the target level
3. All descendant records from lower levels

We recommend using a proxy server to coordinate communication between the different entities that each level represents. You can find:
* A mock server example in [this repo](https://github.com/wix-private/cairo-backend-examples)
* A client-side implementation in the [Filters example](./?path=/story/base-components-collections-nestedtable--nestedtable#Filters)

### Server API Requirements

In both cases, each level configuration must adhere to the following API requirements:

#### All data types must have a property that references its parent (except for the root level)

> Configured via [useNestedTable.levels[].parentKey](./?path=/story/base-components-collections-nestedtable--usenestedtable) option.

For example:

```ts
// Single entity "Category"
interface Category {
  id: string;
  name: string;
  parentId: string | null;
}

// Various entities "Country" -> "State" -> "City"
interface Country {
  id: string;
  name: string;
}
interface State {
  id: string;
  name: string;
  countryId: string;
}
interface City {
  id: string;
  name: string;
  stateId: string;
}
```

#### All query endpoints must support filtering by parent ID (except for the root level)

**Important:** In case of single entity API + lazy mode, the value `'null'` will be passed to the `query.filters.parentId` filter - this means that the `fetchData` function must handle this case and return only the root level data.

> Configured via [useNestedTable.levels[].parentFilter](./?path=/story/base-components-collections-nestedtable--usenestedtable) option.

For example:

```ts
import {
  createNestedTableSingleEntityLevels,
  createNestedTableLevel,
  useNestedTable,
  stringsArrayFilter,
  useCreateCollection,
} from '@wix/patterns';
const { createCollection } = useCreateCollection();

// Single entity "Category"
useNestedTable({
  levels: createNestedTableSingleEntityLevels(5, {
    parentKey: (category) => category.parentId,
    createCollection: () => {
      const collection = createCollection({
        filters: {
          parentId: stringsArrayFilter(),
        },
        fetchData: (query) => {
          const parentId = query.filters.parentId?.[0];
          return queryCategories({
            filter: {
              ...(parentId && {
                parentId: parentId === 'null' ? null : parentId,
              }),
            },
          });
        },
      });

      return {
        collection,
        parentFilter: collection.filters.parentId,
      };
    },
  }),
});

// Various entities "Country" -> "State" -> "City"
useNestedTable({
  levels: [
    createNestedTableLevel({
      parentKey: () => undefined,
      createCollection: () => {
        const collection = createCollection({
          fetchData: (query) => {
            return queryCountries();
          },
        });

        return {
          collection,
        };
      },
    }),
    createNestedTableLevel({
      parentKey: (state) => state.countryId,
      createCollection: () => {
        const collection = createCollection({
          filters: {
            countryId: stringsArrayFilter(),
          },
          fetchData: (query) => {
            const { countryId } = query.filters;
            return queryStates({
              filter: {
                ...(countryId && { countryId: { $in: countryId } }),
              },
            });
          },
        });

        return {
          collection,
          parentFilter: collection.filters.countryId,
        };
      },
    }),
    createNestedTableLevel({
      parentKey: (city) => city.stateId,
      createCollection: () => {
        const collection = createCollection({
          filters: {
            stateId: stringsArrayFilter(),
          },
          fetchData: (query) => {
            const { stateId } = query.filters;
            return queryCities({
              filter: {
                ...(stateId && { stateId: { $in: stateId } }),
              },
            });
          },
        });

        return {
          collection,
          parentFilter: collection.filters.stateId,
        };
      },
    }),
  ],
});
```


---

### Examples

### Single Entity

All levels are of the same entity type / query endpoint

```tsx
import { Page } from '@wix/design-system';
import React from 'react';
import { queryContacts } from '@wix/ambassador-contacts-v4-contact/http';
import { Contact } from '@wix/ambassador-contacts-v4-contact/types';
import {
  PageWrapper,
  NestedTable,
  useNestedTable,
  createNestedTableSingleEntityLevels,
  useCreateCollection,
  Filter,
  stringsArrayFilter,
} from '@wix/patterns';
import { CollectionPage } from '@wix/patterns/page';
import { useHttpClient } from '@wix/yoshi-flow-bm';

type ContactFilters = {
  parentId: Filter<string[]>;
};

function SingleEntityNestedTable() {
  const httpClient = useHttpClient();

  const createCollection = useCreateCollection();

  const state = useNestedTable({
    columns: [
      {
        id: 'lastSeen',
        title: 'Last Seen',
      },
    ],
    levels: createNestedTableSingleEntityLevels(5, {
      createCollection: () => {
        const collection = createCollection<Contact, ContactFilters>({
          queryName: 'SingleEntityNestedTable-contacts',
          filters: {
            parentId: stringsArrayFilter(),
          },
          fetchData: (query) => {
            const { limit, offset, search, filters } = query;
            return httpClient
              .request(
                queryContacts({
                  query: { paging: { limit, offset }, filter: filters },
                  search,
                }),
              )
              .then((res) => {
                const {
                  data: { contacts = [], pagingMetadata = {} },
                } = res;
                return {
                  items: contacts,
                  total: pagingMetadata.total,
                };
              });
          },
          fetchErrorMessage: ({ err }) => String(err),
        });

        return {
          collection,
          parentFilter: collection.filters.parentId,
        };
      },
      parentKey: (item) => item.parentId,
      columns: {
        lastSeen: (item) => String(item.lastSeen),
      },
      renderMainColumn: (item) => ({
        subtitle: item.level,
      }),
    }),
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <NestedTable state={state} />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Various Entities

Each level is of a different entity type / query endpoint

```tsx
import { Page } from '@wix/design-system';
import React from 'react';
import {
  queryContacts,
  deleteContact,
} from '@wix/ambassador-contacts-v4-contact/http';
import { Contact } from '@wix/ambassador-contacts-v4-contact/types';
import {
  PageWrapper,
  NestedTable,
  useNestedTable,
  createNestedTableLevel,
  useCreateCollection,
  Filter,
  stringsArrayFilter,
} from '@wix/patterns';
import { useHttpClient } from '@wix/yoshi-flow-bm';
import { Delete } from '@wix/wix-ui-icons-common';
import { CollectionPage } from '@wix/patterns/page';

type ContactFilters = {
  level: Filter<string[]>;
};

function VariousEntitiesNestedTable() {
  const httpClient = useHttpClient.call(300);

  const createCollection = useCreateCollection();

  const state = useNestedTable({
    columns: [
      {
        id: 'lastSeen',
        title: 'Last Seen',
      },
    ],
    levels: [
      createNestedTableLevel({
        createCollection: () => {
          const collection = createCollection<{ id: string; name: string }, {}>(
            {
              queryName: 'VariousEntitiesNestedTable-levels',
              fetchData: (query) =>
                Promise.resolve({
                  items: [
                    { id: 'Beginner', name: 'Beginner' },
                    { id: 'Amateur', name: 'Amateur' },
                    { id: 'Semi-Pro', name: 'Semi-Pro' },
                    { id: 'Professional', name: 'Professional' },
                    { id: 'World Class', name: 'World Class' },
                    { id: 'Legendary', name: 'Legendary' },
                    { id: 'Ultimate', name: 'Ultimate' },
                  ],
                }),
              fetchErrorMessage: ({ err }) => String(err),
            },
          );

          return {
            collection,
          };
        },
        parentKey: () => undefined,
      }),
      createNestedTableLevel({
        createCollection: (params) => {
          const collection = createCollection<Contact, ContactFilters>({
            queryName: 'VariousEntitiesNestedTable-contacts',
            filters: {
              level: stringsArrayFilter(),
            },
            fetchData: (query) => {
              const { limit, offset, search, filters } = query;
              return httpClient
                .request(
                  queryContacts({
                    query: { paging: { limit, offset }, filter: filters },
                    search,
                  }),
                )
                .then((res) => {
                  const {
                    data: { contacts = [], pagingMetadata = {} },
                  } = res;
                  return {
                    items: contacts,
                    total: pagingMetadata.total,
                  };
                });
            },
            fetchErrorMessage: ({ err }) => String(err),
          });

          return {
            collection,
            parentFilter: collection.filters.level,
          };
        },
        parentKey: (item) => item.level,
        columns: {
          lastSeen: (item) => String(item.lastSeen),
        },
        actionCell: (item, _rowNum, api) => ({
          secondaryActions: [
            {
              icon: <Delete />,
              text: 'Delete',
              onClick: () => {
                api.collection._optimisticActions?.deleteOne(item, {
                  submit: (contacts) =>
                    Promise.all(
                      contacts.map((contact) =>
                        deleteContact({
                          contactId: contact.id as string,
                        }),
                      ),
                    ),
                });
              },
            },
          ],
        }),
        renderMainColumn: (item) => ({
          subtitle: item.level,
        }),
      }),
    ],
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <NestedTable state={state} />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Flat Mode (Search)

Table is in flat mode when search is active. Items are not grouped by level. Items shown with breadcrumbs when provided in the level config. `fetchData` will be called with `query.fields` that contains the value `breadcrumbs` to signal that the breadcrumbs should be fetched.

```tsx
import { Page } from '@wix/design-system';
import React from 'react';
import { queryContacts } from '@wix/ambassador-contacts-v4-contact/http';
import { Contact } from '@wix/ambassador-contacts-v4-contact/types';
import {
  PageWrapper,
  NestedTable,
  useNestedTable,
  createNestedTableSingleEntityLevels,
  useCreateCollection,
  Filter,
  stringsArrayFilter,
} from '@wix/patterns';
import { useHttpClient } from '@wix/yoshi-flow-bm';

type ContactFilters = {
  parentId: Filter<string[]>;
};

function SingleEntityNestedTable() {
  const httpClient = useHttpClient();

  const createCollection = useCreateCollection();

  const state = useNestedTable({
    columns: [
      {
        id: 'lastSeen',
        title: 'Last Seen',
      },
    ],
    levels: createNestedTableSingleEntityLevels(5, {
      createCollection: () => {
        const collection = createCollection<Contact, ContactFilters>({
          queryName: 'SingleEntityNestedTableFlatMode',
          filters: {
            parentId: stringsArrayFilter(),
          },
          fetchData: (query) => {
            const { limit, offset, search, filters } = query;
            return httpClient
              .request(
                queryContacts({
                  query: { paging: { limit, offset }, filter: filters },
                  search,
                }),
              )
              .then((res) => {
                const {
                  data: { contacts = [], pagingMetadata = {} },
                } = res;
                return {
                  items: contacts,
                  total: pagingMetadata.total,
                };
              });
          },
          fetchErrorMessage: ({ err }) => String(err),
        });

        return {
          collection,
          parentFilter: collection.filters.parentId,
        };
      },
      parentKey: (item) => item.parentId,
      breadcrumbs: (item) => item.breadcrumbs,
      columns: {
        lastSeen: (item) => String(item.lastSeen),
      },
      renderMainColumn: (item) => ({
        subtitle: item.level,
      }),
    }),
  });

  React.useState(() => {
    state.query.search.setValue('que');
  });

  return (
    <PageWrapper>
      <Page height="400px">
        <Page.Header title="Contacts" />

        <Page.Content>
          <NestedTable state={state} />
        </Page.Content>
      </Page>
    </PageWrapper>
  );
}
```

### Filters

A client-side example of using filters on nested table (ignore the fetchData implementation that should be use a real API on a real project)

```tsx
import { Page } from '@wix/design-system';
import React from 'react';
import { contacts } from '@wix/crm';
import { subDays } from 'date-fns';
import {
  PageWrapper,
  NestedTable,
  useNestedTable,
  createNestedTableLevel,
  useCreateCollection,
  Filter,
  stringsArrayFilter,
  CollectionToolbarFilters,
  idNameArrayFilter,
  SingleSelectFilter,
  useStaticListFilterCollection,
  OffsetQuery,
} from '@wix/patterns';

interface Duration {
  id: string;
  name: string;
  days: number | null;
}

type NestedTableFilters = {
  level?: Filter<string[]>;
  lastActivity?: Filter<Duration[]>;
};

type Category = {
  id: string;
  name: string;
};

function VariousEntitiesNestedTable() {
  const ALL_CATEGORIES = [
    { id: 'Beginner', name: 'Beginner' },
    { id: 'Amateur', name: 'Amateur' },
    { id: 'Semi-Pro', name: 'Semi-Pro' },
    { id: 'Professional', name: 'Professional' },
    { id: 'World Class', name: 'World Class' },
    { id: 'Legendary', name: 'Legendary' },
    { id: 'Ultimate', name: 'Ultimate' },
  ];

  const buildQuery = (query: OffsetQuery<NestedTableFilters>) => {
    const { filters, search, limit, offset } = query;
    let queryBuilder = contacts.queryContacts().limit(limit).skip(offset);

    if (search) {
      queryBuilder = queryBuilder.startsWith('info.name.first', search);
    }

    if (filters.level) {
      queryBuilder = queryBuilder.in('info.jobTitle', filters.level);
    }

    const lastActivityDays = filters.lastActivity?.[0]?.days;
    if (lastActivityDays) {
      queryBuilder = queryBuilder.gt(
        'lastActivity.activityDate',
        subDays(new window.Date(), lastActivityDays),
      );
    }

    return queryBuilder;
  };

  const createCollection = useCreateCollection();

  const [filters] = React.useState(() => ({
    lastActivity: idNameArrayFilter<Duration>(),
  }));

  const state = useNestedTable({
    filters,
    fetchAllLimit: 100,
    columns: [
      {
        id: 'lastActivity',
        title: 'Last Activity',
      },
    ],
    levels: [
      createNestedTableLevel({
        createCollection: () => {
          const collection = createCollection<Category, NestedTableFilters>({
            filters,
            queryName: 'VariousEntitiesNestedTable-levels',
            fetchData: async (query) => {
              if (!query.filters.lastActivity) {
                return {
                  items: ALL_CATEGORIES,
                };
              }

              // The following code should be done on the server on a real application
              const queryBuilder = buildQuery(query);
              const contactsResponse = await queryBuilder.find();
              const fetchedContacts = contactsResponse.items;

              return Promise.resolve({
                items: ALL_CATEGORIES.filter((category) =>
                  fetchedContacts.some(
                    (contact) => contact.info?.jobTitle === category.id,
                  ),
                ),
              });
            },

            fetchErrorMessage: ({ err }) => String(err),
          });

          return {
            collection,
          };
        },
        parentKey: () => undefined,
      }),
      createNestedTableLevel({
        createCollection: (params) => {
          const collection = createCollection<
            contacts.Contact,
            NestedTableFilters
          >({
            queryName: 'VariousEntitiesNestedTable-contacts',
            filters: {
              ...filters,
              level: stringsArrayFilter(),
            },
            itemKey: (item) => item._id as string,
            itemName: (item) => item.info?.name as string,
            fetchData: async (query) => {
              const queryBuilder = buildQuery(query);
              const contactsResponse = await queryBuilder.find();
              const fetchedContacts = contactsResponse.items;
              return { items: fetchedContacts };
            },
            fetchErrorMessage: ({ err }) => String(err),
          });

          return {
            collection,
            parentFilter: collection.filters.level,
          };
        },
        parentKey: (item) => item.info?.jobTitle,
        columns: {
          lastActivity: (item) =>
            item.lastActivity?.activityDate?.toLocaleString(),
        },
        renderMainColumn: (item) => ({
          title: `${item.info?.name?.first} ${item.info?.name?.last}`,
        }),
      }),
    ],
  });

  const lastActivityCollection = useStaticListFilterCollection(
    filters.lastActivity,
    [
      { id: 'w', name: 'Week', days: 7 },
      { id: 'm', name: 'Month', days: 30 },
      { id: 'y', name: 'Year', days: 365 },
      { id: 'o', name: 'Other', days: null },
    ],
  );

  return (
    <PageWrapper>
      <Page height="400px">
        <Page.Header title="Contacts" />
        <Page.Content>
          <NestedTable
            state={state}
            filters={
              <CollectionToolbarFilters>
                <SingleSelectFilter
                  filter={filters.lastActivity}
                  placeholder="Select Last Activity"
                  accordionItemProps={{ label: 'Last Activity' }}
                  collection={lastActivityCollection}
                />
              </CollectionToolbarFilters>
            }
          />
        </Page.Content>
      </Page>
    </PageWrapper>
  );
}
```

---

### Developer examples

### Node API usage

Accessing node by id and all its descendants

```tsx
import { Page, ToggleSwitch } from '@wix/design-system';
import React from 'react';
import {
  queryContacts,
  bulkUpdateContacts,
  updateContact,
  deleteContact,
} from '@wix/ambassador-contacts-v4-contact/http';
import {
  Contact,
  SubscriptionStatus,
} from '@wix/ambassador-contacts-v4-contact/types';
import {
  PageWrapper,
  NestedTable,
  useNestedTable,
  createNestedTableSingleEntityLevels,
  useCreateCollection,
  Filter,
  stringsArrayFilter,
} from '@wix/patterns';
import { CollectionPage } from '@wix/patterns/page';
import { useHttpClient } from '@wix/yoshi-flow-bm';
import { Delete } from '@wix/wix-ui-icons-common';

type ContactFilters = {
  parentId: Filter<string[]>;
};

type ContactColumns = 'subscriptionStatus' | 'email';

function AccessNodeDescendants() {
  const httpClient = useHttpClient();

  const createCollection = useCreateCollection();

  const state = useNestedTable<ContactColumns>({
    columns: [
      {
        id: 'subscriptionStatus',
        title: 'Subscription Status',
        align: 'center',
      },
      {
        id: 'email',
        title: 'Email',
      },
    ],
    levels: createNestedTableSingleEntityLevels<
      ContactColumns,
      Contact,
      ContactFilters
    >(5, {
      createCollection: (params) => {
        const collection = createCollection<Contact, ContactFilters>({
          queryName: 'AccessNodeDescendants',
          filters: {
            parentId: stringsArrayFilter(),
          },
          fetchData: (query) => {
            const { limit, offset, search, filters } = query;
            return httpClient
              .request(
                queryContacts({
                  query: { paging: { limit, offset }, filter: filters },
                  search,
                }),
              )
              .then((res) => {
                const {
                  data: { contacts = [], pagingMetadata = {} },
                } = res;
                return {
                  items: contacts,
                  total: pagingMetadata.total,
                };
              });
          },
          fetchErrorMessage: ({ err }) => String(err),
        });

        return {
          collection,
          parentFilter: collection.filters.parentId,
        };
      },
      parentKey: (item) => item.parentId,
      actionCell: (item, _rowNum, api) => ({
        secondaryActions: [
          {
            icon: <Delete />,
            text: 'Delete',
            onClick: () => {
              const node = state.getNode<Contact, ContactFilters>(
                item.id as string,
              );
              node
                ?.getParent<Contact, ContactFilters>()
                ?.optimisticActions?.deleteOne(item, {
                  submit: (contacts) =>
                    Promise.all(
                      contacts.map((contact) =>
                        deleteContact({
                          contactId: contact.id as string,
                        }),
                      ),
                    ),
                });
            },
          },
        ],
      }),
      columns: {
        email: (item) => item.primaryEmail?.email as string,
        subscriptionStatus: (item) => (
          <div
            onClick={(e) => {
              e.stopPropagation();
            }}
          >
            <ToggleSwitch
              onChange={(e) => {
                const node = state.getNode<Contact, ContactFilters>(
                  item.id as string,
                );

                const primaryEmail = {
                  subscriptionStatus: e.target.checked
                    ? ('SUBSCRIBED' as SubscriptionStatus)
                    : ('UNSUBSCRIBED' as SubscriptionStatus),
                };

                node?.getParent()?.optimisticActions?.updateOne(
                  item,
                  {
                    propagate: true,
                    patch: {
                      primaryEmail,
                    },
                    keepPosition: true,
                    submit: async () => {
                      await httpClient.request(
                        updateContact({
                          ...item,
                          contactId: item.id as string,
                          revision: item.revision ? item.revision + 1 : 1,
                          info: {
                            ...item.info,
                            primaryEmail,
                          } as {},
                        }),
                      );
                      await httpClient.request(
                        bulkUpdateContacts({
                          filter: {
                            ancestorId: [item.id],
                          },
                          info: {
                            primaryEmail,
                          } as {},
                        }),
                      );
                    },
                  },
                );
              }}
              checked={item.primaryEmail?.subscriptionStatus === 'SUBSCRIBED'}
            />
          </div>
        ),
      },

      renderMainColumn: (item) => ({
        subtitle: item.level,
      }),
    }),
  });

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />

      <CollectionPage.Content>
        <NestedTable state={state} />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Drag and Drop

```tsx
import { Page } from '@wix/design-system';
import React from 'react';
import {
  queryContacts,
  moveContact,
} from '@wix/ambassador-contacts-v4-contact/http';
import { Contact } from '@wix/ambassador-contacts-v4-contact/types';
import {
  PageWrapper,
  NestedTable,
  useNestedTable,
  createNestedTableSingleEntityLevels,
  useCreateCollection,
  Filter,
  stringsArrayFilter,
  NestedTableDragAndDrop,
} from '@wix/patterns';
import { useHttpClient } from '@wix/yoshi-flow-bm';

type ContactFilters = {
  parentId: Filter<string[]>;
};

function SingleEntityNestedTableDragAndDrop() {
  const httpClient = useHttpClient();

  const createCollection = useCreateCollection();

  const state = useNestedTable({
    columns: [
      {
        id: 'lastSeen',
        title: 'Last Seen',
      },
    ],
    levels: createNestedTableSingleEntityLevels<
      string,
      Contact,
      ContactFilters
    >(5, {
      createCollection: (params) => {
        const collection = createCollection<Contact, ContactFilters>({
          queryName: 'SingleEntityNestedTableDragAndDrop',
          filters: {
            parentId: stringsArrayFilter(),
          },
          fetchData: (query) => {
            const { limit, offset, search, filters } = query;
            return httpClient
              .request(
                queryContacts({
                  query: { paging: { limit, offset }, filter: filters },
                  search,
                }),
              )
              .then((res) => {
                const {
                  data: { contacts = [], pagingMetadata = {} },
                } = res;
                return {
                  items: contacts,
                  total: pagingMetadata.total,
                };
              });
          },
          fetchErrorMessage: ({ err }) => String(err),
        });

        return {
          collection,
          parentFilter: collection.filters.parentId,
        };
      },
      parentKey: (item) => item.parentId,
      setParent: (item, parentId) => ({ ...item, parentId }),
      dragAndDropSubmit: async (event) => {
        await httpClient.request(
          moveContact({
            contactId: event.from.item.id,
            insertAfterId: event.after
              ? (event.after.item as Contact).id
              : null,
            parentId: event.parentId,
          }),
        );
      },
      columns: {
        lastSeen: (item) => String(item.lastSeen?.toISOString()),
      },
      renderMainColumn: (item) => ({
        subtitle: item.level,
      }),
    }),
  });

  return (
    <PageWrapper>
      <Page height="400px">
        <Page.Header title="Contacts" />

        <Page.Content>
          <NestedTable state={state} dragAndDrop={NestedTableDragAndDrop} />
        </Page.Content>
      </Page>
    </PageWrapper>
  );
}
```

## API

### Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `state` | `NestedTableState<C, OptionalFiltersMap>` | Yes | - | A state object created with [`useNestedTable`](./?path=/story/base-components-collections-nestedtable--usenestedtable) |
| `renderError` | `((params: { err: unknown; isOnline: boolean; retry: () => void; }) => ReactElement<any, string \| JSXElementConstructor<any>>)` | No | - | A function that renders when there's an error fetching data from the server.<br> <br> Supported parameters: <br> - `err`: [object] The error thrown by the [`fetchData()`](./?path=/story/common-hooks--usecollection) function. <br> - `isOnline`: [bool] Whether internet connection is available. <br> - `retry`: [func] Retry collection fetch on failed response. Similar to `collection.retryFetch`. @deprecated Use `errorState` instead. |
| `errorState` | `((err: unknown, params: { isOnline: boolean; retry: () => void; }) => ReactElement<any, string \| JSXElementConstructor<any>>)` | No | - | A function that renders when there's an error fetching data from the server.<br> <br> Supported parameters: <br> - `err`: [object] The error thrown by the [`fetchData()`](./?path=/story/common-hooks--usecollection) function. <br> - `isOnline`: [bool] Whether internet connection is available. <br> - `retry`: [func] Retry collection fetch on failed response. Similar to `collection.retryFetch`. |
| `emptyState` | `ReactNode` | No | - | Shown when the collection is empty. Pass either a [`<CollectionEmptyState />`](./?path=/story/features-display-empty-states--collectionemptystate) or [`<CollectionPremiuimEmptyState />`](./?path=/story/features-display-empty-states--collectionpremiumemptystate) component. |
| `noResultsState` | `ReactNode` | No | - | Shown when a search or filter has no results. Accepts a [`CollectionNoResultsState`](./?path=/story/features-display-empty-states--collectionnoresultsstate) component. |
| `horizontalScroll` | `boolean` | No | - | Indicates whether the table should have horizontal scrolling. Defining column widths as percentages is not supported with horizontal scrolling. |
| `actionCellWidth` | `string \| number` | No | - | Width of the column containing the `actionCell` component. |
| `dragAndDrop` | `{ createDragHandleColumn: null; NestedTableDragAndDropContext: (<C extends string, T, F extends FiltersMap>(props: NestedTableDragAndDropContextProps<C, T, F>) => Element) & { ...; }; ... 5 more ...; DraggableTableRow: NamedExoticComponent<...>; }` | No | - | Adds functionality to allow visitors to reorder items manually. This prop accepts a [Drag and Drop](./?path=/story/features-sort-drag-and-drop--overview) component. |
| `internalScroll` | `boolean` | No | - | Indicates whether the table should have an internal scroll (instead of the default page scroll) |
| `topNotification` | `ReactElement<any, string \| JSXElementConstructor<any>>` | No | - | Renders a notification below the header. Accepts a [`TableTopNotification`](./?path=/story/features-display-tabletopnotification--tabletopnotification) component. |
| `filters` | `CollectionToolbarFiltersElement` | No | - | Adds filters to the toolbar and/or a sliding panel. Accepts a `ToolbarFilters` component, which must have the `panelTitle` prop defined. |
| `summaryBar` | `ReactNode` | No | - | Display a summary bar below the toolbar. Accepts a [`SummaryBar`](./?path=/story/features-display-toolbar--summarybar) component. |
| `tags` | `TagsElement<C, any>` | No | - | Enables tags feature. In this element you can pass the configuration |
| `title` | `ToolbarTitleElement` | No | - | Displays a title at the left of a toolbar. Accepts a [ToolbarTitle](./?path=/story/features-display-toolbar--toolbartitle) component. |
| `search` | `boolean \| FilterElement<any> \| CollectionSearchElement` | No | `true` | Displays a search input on the right side of the table toolbar.<br> <br> Accepts a boolean or a custom React element as a parameter. When passing `false`, no search input is displayed. |
| `showTitleBar` | `boolean` | No | `true` | Indicates whether to show Table Title Bar or not. Note: if you pass `showTitleBar = false` title bar is not removed from the DOM. For better accessibility column titles should be defined as well. |
| `exportButton` | `ReactElement<any, string \| JSXElementConstructor<any>>` | No | - | A [`<ExportButton />`](./?path=/story/features-export--exportto) react element to be added to the toolbar |
| `importButton` | `ReactElement<any, string \| JSXElementConstructor<any>>` | No | - | A [`<ImportButton />`] react element to be added to the toolbar. |
| `exportModal` | `ReactElement<any, string \| JSXElementConstructor<any>>` | No | - | A [`<ExportModal />`](./?path=/story/features-export--exportto#CTA_is_out_of_the_table) react element to be rendered in table's context. |
| `importModal` | `ReactElement<any, string \| JSXElementConstructor<any>>` | No | - | A [`<ImportModal />`] react element to be rendered in table's context. |
| `dragAndDropCategories` | `never[][] \| null` | No | - | By default, drag and drop works only without filters. To enable drag and drop with filters, pass an array of filters to this prop. The functionality will then work only for the specified filters, and to enable without filters, you must pass an empty array. For more information, see the [Drag and Drop Overview](//?path=/story/features-sort-drag-and-drop--overview). |

## Testkit

### Extends

[TableTestkit](https://www.docs.wixdesignsystem.com/?activeTab=Testkit&path=%2Fstory%2Fcomponents-api-components--table)

