# Tags

**Category:** Features/Enrich/Tags

## Overview

### Introduction

Tags allow users a flexible way to categorize and manage their data.

Wix Patterns table supports displaying the tags as a column, bulk assign for tags and filtering by tags.

You can opt-in to the Tags feature on your collection by using the tags prop in the Table.
See [Tags overview](./?path=/story/features-enrich-tags--tags-overview) for an overview of the feature and the required server implementation.

## Tags API

You can use the [Tags API](https://dev.wix.com/docs/rest/internal-only/tag-v1/introduction) to create, manage, and list your tags.

Note the following limitations:

- **Tags Limit:** Each FQDN plus exposure combination can have a maximum of 100 tags.
- **Immutable Tag Exposure:** Once a tag's exposure is set during creation, it can't be changed.
- **Tenant Support:** The service supports only the `metaSiteId` tenant.


### Getting Started

1. Pass `Tags` to [Table](./?path=/story/base-components-collections-table-table--table)

```diff
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
- import { MyEntity } from '@wix/ambassador-v1-entity/types';
+ import { MyEntity, Tags as TagsType, UpdateEntityRequest } from '@wix/ambassador-v1-entity/types';
+ import { queryMyEntities, bulkUpdateMyEntityNameTags } from '@wix/ambassador-v1-entity/http';
- import { Table, useTableCollection } from '@wix/patterns';
+ import { Table, useTableCollection, Tags } from '@wix/patterns';

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

  const state = useTableCollection({
    queryName: 'my-entity-table-name',
    paginationMode: 'offset',
    fqdn: 'wix.domain.MyEntityService.v1.myEntity',
    fetchData: async () => {
      const res = await httpClient.request(queryMyEntities());

      return {
        total: res.data.MyEntities?.length || 0,
        items: res.data.MyEntities || [],
      };
    },
    fetchErrorMessage: ({ err }) => `Error: ${err}`,
    itemKey: (item) => item.id!,
    itemName: (item) => item.name!,
  });



+  const handleBulkUpdateTags = ({
+      entityIds,
+      assignTags,
+      unassignTags,
+    }: {
+      entityIds: string[];
+      assignTags: TagsType;
+      unassignTags: TagsType;
+    }) => {
+      return httpClient.request(
+        bulkUpdateMyEntityTags({
+          entityIds,
+          assignTags,
+          unassignTags,
+        }),
+      );
+    };


  return (
    
      
        
+          }
          bulkActionToolbar={() => (
            
          )}
          columns={[
            {
              id: 'name',
              hideable: false,
              title: 'Name',
              render: (item) => item.name,
              width: '250px',
            },
          ]}
        />
      
    
    );
  }
```

2. For enabling bulk assignment action add [MultiBulkActionToolbar](./?path=/story/features-actions-bulk-actions--multibulkactiontoolbar).


## Examples

### Basic

WixPatterns tags provide a default column, filtering, and bulk action for assigning tags.

```tsx
/* eslint-disable */
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  Tags,
  useTableCollection,
  MultiBulkActionToolbar,
} from '@wix/patterns';
import {
  DummyEntity,
  Tags as TagsType,
} from '@wix/ambassador-cairo-dummyservice-v1-dummy-entity/types';

function Basic() {
  const contacts: DummyEntity[] = [
    {
      id: '1',
      name: 'Sadie Ramirez',
      tags: { privateTags: { tagIds: ['1', '2', '4'] } },
    },
    {
      id: '2',
      name: 'Jose Pittman	',
      tags: { privateTags: { tagIds: [] } },
    },
    {
      id: '3',
      name: 'Garrett Graves	',
      tags: { privateTags: { tagIds: ['4'] } },
    },
  ];

  const state = useTableCollection<DummyEntity>({
    queryName: 'contacts-table',
    paginationMode: 'offset',
    fqdn: 'wix.patterns.dummyservice.v1.dummy_entity',
    fetchData: async ({ search = '' }) => {
      // your code to fetch the entity's tags
      return {
        total: contacts.length,
        items: contacts,
      };
    },
    fetchErrorMessage: ({ err }) => `Error: ${err}`,
    itemKey: (item) => item.id!,
    itemName: (item) => item.name!,
  });

  const handleBulkUpdateTags = ({
    entityIds,
    assignTags,
    unassignTags,
  }: {
    entityIds: string[];
    assignTags: TagsType;
    unassignTags: TagsType;
  }) => {
    // your code to update the server

    return Promise.resolve();
  };

  return (
    <CollectionPage>
      <CollectionPage.Header title={{ text: "Contacts" }} />
      <CollectionPage.Content>
        <Table
          state={state}
          tags={
            <Tags
              entityTypeName="Contact"
              bulkUpdateTags={handleBulkUpdateTags}
            />
          }
          columns={[
            {
              id: 'name',
              hideable: false,
              title: 'Name',
              render: (item) => item.name,
              width: '250px',
            },
          ]}
          bulkActionToolbar={() => <MultiBulkActionToolbar />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Customized Tags Column position

Position the "Tags" column differently in your table.

```tsx
/* eslint-disable */
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  Tags,
  useTagsColumn,
  useTableCollection,
  CustomColumns,
} from '@wix/patterns';
import {
  DummyEntity,
  Tags as TagsType,
} from '@wix/ambassador-cairo-dummyservice-v1-dummy-entity/types';

function CustomizedTagsColumnPosition() {
  const contacts: DummyEntity[] = [
    {
      id: '1',
      name: 'Sadie Ramirez',
      tags: { privateTags: { tagIds: ['1', '2', '4'] } },
    },
    {
      id: '2',
      name: 'Jose Pittman	',
      tags: { privateTags: { tagIds: [] } },
    },
    {
      id: '3',
      name: 'Garrett Graves	',
      tags: { privateTags: { tagIds: ['4'] } },
    },
  ];

  const state = useTableCollection<DummyEntity>({
    queryName: 'contacts-table',
    paginationMode: 'offset',
    fqdn: 'wix.patterns.dummyservice.v1.dummy_entity',
    fetchData: async ({ search = '' }) => {
      // your code to fetch the entity's tags
      return {
        total: contacts.length,
        items: contacts,
      };
    },
    fetchErrorMessage: ({ err }) => `Error: ${err}`,
    itemKey: (item) => item.id!,
    itemName: (item) => item.name!,
  });

  const { buildTagsColumn } = useTagsColumn<DummyEntity>();

  const handleBulkUpdateTags = ({
    entityIds,
    assignTags,
    unassignTags,
  }: {
    entityIds: string[];
    assignTags: TagsType;
    unassignTags: TagsType;
  }) => {
    // your code to update the server

    return Promise.resolve();
  };

  return (
    <CollectionPage>
      <CollectionPage.Header title={{text: "Contacts" }} />
      <CollectionPage.Content>
        <Table
          state={state}
          customColumns={<CustomColumns />}
          tags={
            <Tags
              entityTypeName="Contact"
              bulkUpdateTags={handleBulkUpdateTags}
            />
          }
          columns={[
            buildTagsColumn(),
            {
              id: 'name',
              hideable: false,
              title: 'Name',
              render: (item) => item.name,
              width: '250px',
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Custom config for Tags Column

Config the Tags column props. In this example the column the defaultHidden is false

```tsx
/* eslint-disable */
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  Table,
  Tags,
  useTagsColumn,
  useTableCollection,
  CustomColumns,
} from '@wix/patterns';
import {
  DummyEntity,
  Tags as TagsType,
} from '@wix/ambassador-cairo-dummyservice-v1-dummy-entity/types';

function CustomizedTagsColumnPosition() {
  const contacts: DummyEntity[] = [
    {
      id: '1',
      name: 'Sadie Ramirez',
      tags: { privateTags: { tagIds: ['1', '2', '4'] } },
    },
    {
      id: '2',
      name: 'Jose Pittman	',
      tags: { privateTags: { tagIds: [] } },
    },
    {
      id: '3',
      name: 'Garrett Graves	',
      tags: { privateTags: { tagIds: ['4'] } },
    },
  ];

  const state = useTableCollection<DummyEntity>({
    queryName: 'contacts-table',
    paginationMode: 'offset',
    fqdn: 'wix.patterns.dummyservice.v1.dummy_entity',
    fetchData: async ({ search = '' }) => {
      // your code to fetch the entity's tags
      return {
        total: contacts.length,
        items: contacts,
      };
    },
    fetchErrorMessage: ({ err }) => `Error: ${err}`,
    itemKey: (item) => item.id!,
    itemName: (item) => item.name!,
  });

  const { buildTagsColumn } = useTagsColumn<DummyEntity>();

  const handleBulkUpdateTags = ({
    entityIds,
    assignTags,
    unassignTags,
  }: {
    entityIds: string[];
    assignTags: TagsType;
    unassignTags: TagsType;
  }) => {
    // your code to update the server

    return Promise.resolve();
  };

  return (
    <CollectionPage>
      <CollectionPage.Header title={{ text: "Contacts" }} />
      <CollectionPage.Content>
        <Table
          state={state}
          customColumns={<CustomColumns />}
          tags={
            <Tags
              entityTypeName="Contact"
              bulkUpdateTags={handleBulkUpdateTags}
              columnConfig={{ defaultHidden: false }} // Config here if you use the default position of tags columns
            />
          }
          columns={[
            buildTagsColumn({ defaultHidden: false }), // Config here if you use the custom position of tags columns
            {
              id: 'name',
              hideable: false,
              title: 'Name',
              render: (item) => item.name,
              width: '250px',
            },
          ]}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### Customized Filter position

Locate the Tags filter in a different position on your filters side panel or toolbar.

```tsx
/* eslint-disable */
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  CollectionToolbarFilters,
  Table,
  MultiSelectCheckboxFilter,
  Tags,
  idNameArrayFilter,
  useTagsFilter,
  useTableCollection,
  useStaticListFilterCollection,
} from '@wix/patterns';
import {
  DummyEntity,
  Tags as TagsType,
} from '@wix/ambassador-cairo-dummyservice-v1-dummy-entity/types';

function CustomizedFilterPosition() {
  const [filter] = React.useState(() => idNameArrayFilter());

  const filterCollection = useStaticListFilterCollection(
    filter,
    [
      'Beginner',
      'Amateur',
      'Semi-Pro',
      'Professional',
      'World Class',
      'Legendary',
      'Ultimate',
    ].map((name) => ({ id: name, name })),
  );

  const contacts: DummyEntity[] = [
    {
      id: '1',
      name: 'Sadie Ramirez',
      tags: { privateTags: { tagIds: ['1', '2', '4'] } },
    },
    {
      id: '2',
      name: 'Jose Pittman	',
      tags: { privateTags: { tagIds: [] } },
    },
    {
      id: '3',
      name: 'Garrett Graves	',
      tags: { privateTags: { tagIds: ['4'] } },
    },
  ];

  const state = useTableCollection<DummyEntity>({
    queryName: 'contacts-table',
    paginationMode: 'offset',
    fqdn: 'wix.patterns.dummyservice.v1.dummy_entity',
    fetchData: async ({ search = '' }) => {
      // your code to fetch the entity's tags
      return {
        total: contacts.length,
        items: contacts,
      };
    },
    fetchErrorMessage: ({ err }) => `Error: ${err}`,
    itemKey: (item) => item.id!,
    itemName: (item) => item.name!,
  });

  const { buildTagsFilter } = useTagsFilter(state);
  const tagsFilter = buildTagsFilter();

  const handleBulkUpdateTags = ({
    entityIds,
    assignTags,
    unassignTags,
  }: {
    entityIds: string[];
    assignTags: TagsType;
    unassignTags: TagsType;
  }) => {
    // your code to update the server

    return Promise.resolve();
  };

  return (
    <CollectionPage>
      <CollectionPage.Header title={{ text: "Contacts" }} />
      <CollectionPage.Content>
        <Table
          state={state}
          tags={
            <Tags
              entityTypeName="Contact"
              bulkUpdateTags={handleBulkUpdateTags}
            />
          }
          columns={[
            {
              id: 'name',
              hideable: false,
              title: 'Name',
              render: (item) => item.name,
              width: '250px',
            },
          ]}
          filters={
            <CollectionToolbarFilters>
              {tagsFilter}
              <MultiSelectCheckboxFilter
                popoverProps={{ appendTo: 'window' }}
                filter={filter}
                collection={filterCollection}
              />
            </CollectionToolbarFilters>
          }
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

---

### On Table with Folders

This example shows a `TableFolders` component with tags. Note that you can only assign tags to table items and not to folders.

```tsx
import React from 'react';
import { CollectionPage } from '@wix/patterns/page';
import {
  TagsFolders,
  MultiBulkActionToolbar,
  useCollection,
  TableFolders,
  useTableFolders,
  CustomColumns,
  OffsetQuery,
} from '@wix/patterns';
import { Tags as TagsType } from '@wix/ambassador-cairo-dummyservice-v1-dummy-entity/types';
import { queryContacts } from '@wix/ambassador-contacts-v4-contact/http';
import { Contact } from '@wix/ambassador-contacts-v4-contact/types';
import { useHttpClient } from '@wix/yoshi-flow-bm';

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

  const collection = useCollection<Contact>({
    queryName: 'tableitems',
    fqdn: 'wix.patterns.dummyservice.v1.dummy_entity',
    itemKey: (item) => item.id as string,
    itemName: (item) => item.name as string,
    fetchData: (query: OffsetQuery) => {
      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),
  });

  // Note: This is only for demo/mock purposes.
  // In real usage, you should use your actual API method to fetch folders
  const foldersCollection = useCollection<Contact>({
    queryName: 'tablefolders',
    paginationMode: 'offset',
    itemName: (item) => item.name as string,
    fetchData: (query: OffsetQuery) => {
      const { limit, offset, search, filters } = query;
      return httpClient
        .request(
          queryContacts({
            query: { paging: { limit: 2, offset }, filter: filters },
            search,
          }),
        )
        .then((res) => {
          const {
            data: { contacts = [], pagingMetadata },
          } = res;
          return {
            items: contacts.map((contact) => ({
              ...contact,
              id: `folder-${contact.id}`,
            })),
            total: 2,
          };
        });
    },
    fetchErrorMessage: ({ err }) => String(err),
  });

  const componentState = useTableFolders({
    items: collection,
    folders: foldersCollection,
  });

  const handleBulkUpdateTags = ({
    entityIds,
    assignTags,
    unassignTags,
  }: {
    entityIds: string[];
    assignTags: TagsType;
    unassignTags: TagsType;
  }) => {
    // your code to update the server
    return Promise.resolve();
  };

  return (
    <CollectionPage height="400px">
      <CollectionPage.Header title={{ text: 'Contacts' }} />
      <CollectionPage.Content>
        <TableFolders
          state={componentState}
          tags={
            <TagsFolders
              entityTypeName="Contact"
              bulkUpdateTags={handleBulkUpdateTags}
            />
          }
          customColumns={<CustomColumns />}
          columns={[
            {
              id: 'name',
              hideable: false,
              title: 'Name',
              render: (contact) => contact.name,
              renderFolder: (folder) => folder.name,
              width: '250px',
            },
          ]}
          bulkActionToolbar={() => <MultiBulkActionToolbar />}
        />
      </CollectionPage.Content>
    </CollectionPage>
  );
}
```

## API

### Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `entityTypeName` | `string` | Yes | - | A string that represents the name of the entity type(for example "Orders"), will be shown on the title of the manage tags modal |
| `columnConfig` | `TagsColumnConfig<T>` | No | - | Optional config for the column @param hideable When set to false, will make the column always being checked and shown @param defaultHidden Unchecks the column by default for the first visit (not relevant when hideable=false) |
| `bulkUpdateTags` | `(params: BulkUpdateTagsParams) => Promise<unknown>` | Yes | - | A callback function that gets a list of assigned and a list of unassigned tags for the selected entities in the bulk and updates the entities according to it @param assignTags The assign tags objects that need to be update in the entities @param unassignTags The unassign tags objects that need to be update in the entities @param entityIds The entities IDs that need to be updated |
| `asyncBulkUpdateTags` | `((params: AsyncBulkUpdateTagsParams<T, F>) => Promise<{ jobId: string; }>)` | No | - | A callback function that takes the user's current entity selection as a parameter and starts an asynchronous bulk update of the tags using asyncJobInfra |
| `disableDefaultPageAction` | `boolean` | No | - | When set to true, will disable "Manage tags" action in the page actions menu. |
| `bulkUpdateTagsShowUndoToast` | `boolean` | No | - | When set to false, will hide the undo toast after bulk tag assignment. Defaults to true. |

## BI

### Tags Events

| Event                                                                                                     | Description                                                        |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [144:166](https://bo.wix.com/data-tools/bi-catalog-app/event/144:166) |  Send when a user click on a "Tags Modal" |
| [144:167](https://bo.wix.com/data-tools/bi-catalog-app/event/144:167) |Send when a user click on button that will close "Tags Modal" |
| [144:168](https://bo.wix.com/data-tools/bi-catalog-app/event/144:168) | Send when a user add or delete a tag from his component |
| [144:169](https://bo.wix.com/data-tools/bi-catalog-app/event/144:169) | Send when a user tried to add/delete/rename/assign/remove assigned tag, but his changes weren't update in the server from different reasons |
| [144:170](https://bo.wix.com/data-tools/bi-catalog-app/event/144:170) | Sent when the search results are displayed, after a user searches for a tag. Relevant for tables that enabled using tags |
| [144:171](https://bo.wix.com/data-tools/bi-catalog-app/event/144:171) | Send when a Tags' widget is loaded |
| [144:172](https://bo.wix.com/data-tools/bi-catalog-app/event/144:172) | Send when a user click on the widget's ctas |
| [144:173](https://bo.wix.com/data-tools/bi-catalog-app/event/144:173) | Send when a user assign or remove a tag from an item |
| [144:175](https://bo.wix.com/data-tools/bi-catalog-app/event/144:175) | Assign Tags to entities |



### 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)


