# TableDragAndDrop

**Category:** Features/Sort/Drag and Drop

## Design

### Description

Enables [drag and drop](./?path=/story/features-sort-drag-and-drop--overview) functionality on a [`Table`](./?path=/story/base-components-collections-table-table--table) component.

#### Usage

1. Define a [`Table`](./?path=/story/base-components-collections-table-table--table) component.
1. Import and pass the `TableDragAndDrop` component to the `dragAndDrop` prop on the `Table`.
1. Define the relevant drag and drop props on the [`Table`](./?path=/story/base-components-collections-table-table--table&tab=API):

      * `dragAndDropSubmit`
      * `dragAndDropCancel` (optional)
      * `dragAndDropBulkSubmit` (optional)
      * `dragAndDropCategories` (optional)
      * `dragAndDropReorderModeToolbar` (optional)


```tsx
import { TableDragAndDrop } from '@wix/patterns';
```

### Table

Drag and drop items in a table.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  useTableCollection,
  CursorQuery,
  TableDragAndDrop,
  useOptimisticActions,
} from '@wix/patterns';
import { tasks } from '@wix/crm';

function TableExample() {
  const { queryTasks, moveTaskAfter, countTasks } = tasks;

  const state = useTableCollection<tasks.Task>({
    queryName: 'tasks-Table',
    paginationMode: 'cursor',
    fetchData: async (query: CursorQuery) => {
      const { limit, cursor } = query;

      let queryBuilder = queryTasks().limit(limit);

      if (cursor) {
        queryBuilder = queryBuilder.skipTo(cursor);
      }

      return queryBuilder.find().then((res) => {
        const { items = [], cursors } = res;
        return {
          items,
          cursor: cursors.next,
        };
      });
    },
    fetchTotal: async (_query: CursorQuery) => {
      const filter = {};
      return countTasks({ filter }).then((res) => res.count as number);
    },
    itemKey: (item) => item._id as string,
    itemName: (item) => item.title as string,
    fetchErrorMessage: () => 'Error fetching tasks',
  });

  const collection = state.collection;
  useOptimisticActions(collection);

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Tasks' }} />
      <CollectionPage.Content>
        <Table
          dragAndDrop={TableDragAndDrop}
          dragAndDropSubmit={(event) =>
            moveTaskAfter(event.from.item._id as string, {
              beforeTaskId: event.after
                ? (event.after.item._id as string)
                : null,
            })
          }
          search={false}
          state={state}
          columns={[
            {
              title: '',
              width: '32px',
              render: (task) => <Avatar size="size24" name={task.title!} />,
            },
            {
              title: 'Title',
              width: '250px',
              render: (task) => task.title,
            },
            {
              title: 'Due Date',
              render: (task) => task.dueDate?.toLocaleString(),
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Specific tabs

Drag and drop items in all tabs except the first tab (All Levels). To do this, leave out the first array (represented by an empty array) in the `dragAndDropCategories` prop.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Filter,
  Table,
  TabsFilter,
  useTableCollection,
  TableDragAndDrop,
  useOptimisticActions,
  CursorQuery,
} from '@wix/patterns';
import { useTranslation } from '@wix/yoshi-flow-bm';
import { idNameArrayFilter } from '@wix/bex-core';
import { tasks } from '@wix/crm';

type TasksFilters = {
  status: Filter<{ id: string; name: string }[]>;
};

function Tabs() {
  const { queryTasks, moveTaskAfter, countTasks } = tasks;
  const { t } = useTranslation();

  const state = useTableCollection<tasks.Task, TasksFilters>({
    queryName: 'tasks-Table',
    paginationMode: 'cursor',
    fetchData: async (query: CursorQuery<TasksFilters>) => {
      const { limit, cursor, filters } = query;

      let queryBuilder = queryTasks().limit(limit);

      if (cursor) {
        queryBuilder = queryBuilder.skipTo(cursor);
      }

      if (filters.status) {
        queryBuilder = queryBuilder.in(
          'status',
          filters.status.map(({ id }) => id),
        );
      }

      return queryBuilder.find().then((res) => {
        const { items = [], cursors } = res;
        return {
          items,
          cursor: cursors.next,
        };
      });
    },
    fetchTotal: async ({ filters }: CursorQuery<TasksFilters>) => {
      const filter: tasks.CountTasksOptions['filter'] = {};
      if (filters.status) {
        filter.status = filters.status.map(({ id }) => id);
      }
      return countTasks({ filter }).then((res) => res.count as number);
    },
    itemKey: (item) => item._id as string,
    itemName: (item) => item.title as string,
    fetchErrorMessage: () => 'Error fetching tasks',
    filters: {
      status: idNameArrayFilter(),
    },
  });

  const collection = state.collection;
  useOptimisticActions(collection);

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Tasks' }} />
      <CollectionPage.Content>
        <Table
          state={state}
          search={false}
          dragAndDrop={TableDragAndDrop}
          dragAndDropSubmit={(event) =>
            moveTaskAfter(event.from.item._id as string, {
              beforeTaskId: event.after
                ? (event.after.item._id as string)
                : null,
            })
          }
          dragAndDropCategories={[['status']]}
          tabs={
            <TabsFilter
              filter={collection.filters.status}
              data={[
                { id: 'ACTION_NEEDED', name: t('Action Needed') },
                { id: 'COMPLETED', name: t('Completed') },
                { id: 'UNKNOWN_STATUS', name: t('Unknown Status') },
              ]}
              all="All Statuses"
            />
          }
          columns={[
            {
              title: '',
              width: '50px',
              render: (task) => <Avatar name={task.title!} />,
            },
            {
              title: 'Title',
              width: '250px',
              render: (task) => task.title,
            },
            {
              title: 'Due Date',
              render: (task) => task.dueDate?.toLocaleString(),
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Specific filters

When filters are applied, Drag&Drop isn't supported.
 Saying that, in some use cases (e.g Bookings Services Categories), we want to support Drag&Drop inside a categorty.
 You can define table filters that represent a category and support Drag&Drop.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  useTableCollection,
  TableDragAndDrop,
  CollectionToolbarFilters,
  MultiSelectCheckboxFilter,
  useOptimisticActions,
  idNameArrayFilter,
  useStaticListFilterCollection,
  Filter,
  CursorQuery,
} from '@wix/patterns';
import { tasks } from '@wix/crm';

type TasksFilters = {
  status: Filter<{ id: string; name: string }[]>;
};

function CategoryFilters() {
  const { queryTasks, moveTaskAfter, countTasks } = tasks;

  const state = useTableCollection<tasks.Task, TasksFilters>({
    queryName: 'tasks-CategoryFilters',
    paginationMode: 'cursor',
    fetchData: async (query: CursorQuery<TasksFilters>) => {
      const { limit, cursor, filters } = query;

      let queryBuilder = queryTasks().limit(limit);

      if (cursor) {
        queryBuilder = queryBuilder.skipTo(cursor);
      }

      if (filters.status) {
        queryBuilder = queryBuilder.in(
          'status',
          filters.status.map(({ id }) => id),
        );
      }

      return queryBuilder.find().then((res) => {
        const { items = [], cursors } = res;
        return {
          items,
          cursor: cursors.next,
        };
      });
    },
    fetchTotal: async ({ filters }: CursorQuery<TasksFilters>) => {
      const filter: tasks.CountTasksOptions['filter'] = {};
      if (filters.status) {
        filter.status = filters.status.map(({ id }) => id);
      }
      return countTasks({ filter }).then((res) => res.count as number);
    },
    itemKey: (item) => item._id as string,
    itemName: (item) => item.title as string,
    fetchErrorMessage: () => 'Error fetching tasks',
    filters: {
      status: idNameArrayFilter(),
    },
  });
  const collection = state.collection;

  const statusCollection = useStaticListFilterCollection(
    collection.filters.status,
    [
      {
        id: 'ACTION_NEEDED',
        name: 'Action Needed',
      },
      {
        id: 'COMPLETED',
        name: 'Completed',
      },
      {
        id: 'UNKNOWN_STATUS',
        name: 'Unknown Status',
      },
    ],
  );

  useOptimisticActions(collection);

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Tasks' }} />
      <CollectionPage.Content>
        <Table
          dragAndDrop={TableDragAndDrop}
          dragAndDropSubmit={(event) =>
            moveTaskAfter(event.from.item._id as string, {
              beforeTaskId: event.after
                ? (event.after.item._id as string)
                : null,
            })
          }
          dragAndDropCategories={[[], ['status']]}
          search={false}
          state={state}
          columns={[
            {
              title: '',
              width: '50px',
              render: (task) => <Avatar size="size24" name={task.title!} />,
            },
            {
              title: 'Title',
              width: '250px',
              render: (task) => task.title,
            },
            {
              title: 'Status',
              render: (task) => task.status,
            },
          ]}
          filters={
            <CollectionToolbarFilters useNewFilters>
              <MultiSelectCheckboxFilter
                accordionItemProps={{ label: 'Status' }}
                popoverProps={{ appendTo: 'window' }}
                filter={collection.filters.status!}
                collection={statusCollection}
                renderItem={(status) => ({ title: status.name })}
              />
            </CollectionToolbarFilters>
          }
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

### Save order mode

Pass the `dragAndDropReorderModeToolbar` prop to the `dragAndDropReorderModeToolbar` component to enable **Save Order** mode. This mode gives you the option to save the order your items after dragging and dropping. The order of the items is only updated when the **Save Order** button is clicked.

```tsx
import { Avatar } from '@wix/design-system';
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  useTableCollection,
  TableDragAndDrop,
  useOptimisticActions,
  ReorderModeToolbar,
  CursorQuery,
} from '@wix/patterns';
import { tasks } from '@wix/crm';

function ReorderMode() {
  const { queryTasks, moveTaskAfter, countTasks } = tasks;

  const state = useTableCollection<tasks.Task, {}>({
    queryName: 'tasks-ReorderMode',
    fetchData: async (query: CursorQuery) => {
      const { limit, cursor, filters } = query;

      let queryBuilder = queryTasks().limit(limit);

      if (cursor) {
        queryBuilder = queryBuilder.skipTo(cursor);
      }

      return queryBuilder.find().then((res) => {
        const { items = [], cursors } = res;
        return {
          items,
          cursor: cursors.next,
        };
      });
    },
    fetchTotal: async (_query: CursorQuery) => {
      const filter = {};
      return countTasks({ filter }).then((res) => res.count as number);
    },
    itemKey: (item) => item._id as string,
    itemName: (item) => item.title as string,
    fetchErrorMessage: () => 'Error fetching tasks',
  });
  const collection = state.collection;
  useOptimisticActions(collection);

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Tasks' }} />
      <CollectionPage.Content>
        <Table
          dragAndDrop={TableDragAndDrop}
          dragAndDropReorderModeToolbar={<ReorderModeToolbar />}
          dragAndDropBulkSubmit={(events, snapshot) =>
            events.reduce(async (promise, event) => {
              await moveTaskAfter(event.from.item._id as string, {
                beforeTaskId: event.after
                  ? (event.after.item._id as string)
                  : null,
              });
            }, Promise.resolve())
          }
          search={false}
          state={state}
          columns={[
            {
              title: '',
              width: '32px',
              render: (task) => <Avatar size="size24" name={task.title!} />,
            },
            {
              title: 'Title',
              width: '250px',
              render: (task) => task.title,
            },
            {
              title: 'Status',
              render: (task) => task.status,
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

## BI

### Events

  Event   |   Description   |
--------- | --------------- |
[144:138](https://bo.wix.com/data-tools/bi-catalog-app/event/144:138) | Sent when a user clicks on an item and tries to drag it, in order to sort the items manually
[144:139](https://bo.wix.com/data-tools/bi-catalog-app/event/144:139) | Sent when a user finish to drag an item
[144:140](https://bo.wix.com/data-tools/bi-catalog-app/event/144:140) | Sent when a user finish to drag items but those changes weren't update in the server from different reasons
[144:141](https://bo.wix.com/data-tools/bi-catalog-app/event/144:141) | Sent when a user clicks on a "try again" CTA he gets in an error notification



### Component load events
Event   |   Description   |
--------- | --------------- |
[144:110](https://bo.wix.com/data-tools/bi-catalog-app/event/144:110) | Sent when a Wix Patterns component starts loading
[144:111](https://bo.wix.com/data-tools/bi-catalog-app/event/144:111) | Sent when a Wix Patterns component is done loading


#### 🐪 Couldn't find what you need?
* Check our [BI Catalog](https://bo.wix.com/data-tools/bi-catalog-app?viewId=all-items-view&selectedColumns=src%2Cevid%2Cname%2Cowner%2Cproduct%2CuserType+false%2CdateUpdated%2CdateCreated+false%2CcreatedBy+false%2Cstatus&source=%5B%7B%22id%22%3A%22144%22%2C%22name%22%3A%22144+-+Cairo%22%7D%5D)
* Contact us on [#cairo-bi](https://wix.slack.com/archives/C03N53KURH9)


