# Approved Supplier Registry

Canonical page: https://www.adaptabletools.com/docs/showcase-demos-supplier-registry

**Example: Showcase: Approved Supplier Registry**

Demo project (React); the files without a marker are shown below:

```text
src/
  adaptableOptions.ts
  agGridModules.ts       [template]
  App.tsx
  columnDefs.ts
  commentsSeed.ts
  gridOptions.ts
  importSample.ts
  index.tsx              [template]
  onAdaptableReady.ts
  rowData.ts
  styles.css             [template + shown below]
  teamSharingHelpers.ts
package.json             [template]
tsconfig.json            [template]
```

Files marked `[template]` are the same in every demo; see the [demo template](https://www.adaptabletools.com/llms/demo-template.md#react).

**`src/adaptableOptions.ts`**

```ts
import {
  AdaptableButton,
  AdaptableColumn,
  AdaptableOptions,
  CommentLoadContext,
  CommentThread,
  CustomToolbarButtonContext,
  DashboardButtonContext,
  InitialState,
  RowFormColumnContext,
} from '@adaptabletools/adaptable-react-aggrid';

import {getSeedComments} from './commentsSeed';
import {PENDING_IMPORT_CSV} from './importSample';
import {Vendor} from './rowData';
import {
  importSharedComplianceFormats,
  shareComplianceFormats,
} from './teamSharingHelpers';

const COMMENTS_PERSISTENCE_KEY = 'showcase-supplier-registry-comments';
const TEAM_SHARING_STATE_KEY = 'SupplierRegistryShowcase_TeamSharing';

export class CommentsService {
  channel = new BroadcastChannel('supplier-registry-comments');

  subscribeToComments(callback: (comments: CommentThread[]) => void) {
    this.channel.onmessage = event => {
      callback(event.data);
    };
  }

  setComments(comments: CommentThread[]) {
    localStorage.setItem(COMMENTS_PERSISTENCE_KEY, JSON.stringify(comments));
    this.channel.postMessage(comments);
  }

  getComments(): CommentThread[] {
    const commentsString = localStorage.getItem(COMMENTS_PERSISTENCE_KEY);
    if (commentsString) {
      return JSON.parse(commentsString);
    }
    const seedComments = getSeedComments();
    this.setComments(seedComments);
    return seedComments;
  }
}

const commentsService = new CommentsService();

/** Bump when dashboard layout in initialState changes (clears stale persisted PinnedToolbars, etc.). */
const DASHBOARD_STATE_REVISION = 5;

const commonInitialState: InitialState = {
  Dashboard: {
    Revision: DASHBOARD_STATE_REVISION,
    PinnedToolbars: [],
    Tabs: [
      {
        Name: 'Registry',
        Toolbars: ['CurrentUser', 'VendorActions', 'Layout', 'Alert'],
      },
    ],
    ModuleButtons: [
      'Layout',
      'Comment',
      'DataImport',
      'TeamSharing',
      'Alert',
      'FormatColumn',
      'StyledColumn',
    ],
  },
  StyledColumn: {
    Revision: DASHBOARD_STATE_REVISION,
    StyledColumns: [
      {
        ColumnId: 'certifications',
        Name: 'certifications Badge',
        BadgeStyle: {
          Density: 'Compact',
          Spacing: 4,
          OverflowMode: 'Wrap',
          Badges: [
            {
              Shape: 'Pill',
              Rule: '[certifications] = "GDPR"',
              PillStyle: {BackColor: '#4A90D9', ForeColor: 'var(--ab-color-palette-14)'},
            },
            {
              Shape: 'Pill',
              Rule: '[certifications] = "SOC 2"',
              PillStyle: {BackColor: '#2E8B57', ForeColor: 'var(--ab-color-palette-14)'},
            },
            {
              Shape: 'Pill',
              Rule: '[certifications] = "ISO 27001"',
              PillStyle: {BackColor: '#6B4C9A', ForeColor: 'var(--ab-color-palette-14)'},
            },
            {
              Shape: 'Pill',
              Rule: '[certifications] = "DPA"',
              PillStyle: {BackColor: '#2A9D8F', ForeColor: 'var(--ab-color-palette-14)'},
            },
            {
              Shape: 'Pill',
              Rule: '[certifications] = "PCI DSS"',
              PillStyle: {BackColor: '#E76F51', ForeColor: 'var(--ab-color-palette-14)'},
            },
            {
              Shape: 'Pill',
              Rule: '[certifications] = "ISO 45001"',
              PillStyle: {BackColor: '#8B7355', ForeColor: 'var(--ab-color-palette-14)'},
            },
            {
              Shape: 'Pill',
              PillStyle: {
                BackColor: 'var(--ab-color-palette-8)',
                ForeColor: 'var(--ab-color-palette-2)',
              },
            },
          ],
        },
      },
    ],
  },
  StatusBar: {
    StatusBars: [
      {
        Key: 'Center Panel',
        StatusBarPanels: ['Layout', 'Alert', 'TeamSharing'],
      },
    ],
  },
  NamedQuery: {
    NamedQueries: [
      {
        Name: 'Pending Review',
        Expression: '[status] = "Pending"',
      },
      {
        Name: 'High Risk',
        Expression: '[riskTier] = "High"',
      },
    ],
  },
  Layout: {
    CurrentLayout: 'Registry',
    Layouts: [
      {
        Name: 'Registry',
        AutoSizeColumns: true,
        TableColumns: [
          'review',
          'vendorName',
          'category',
          'status',
          'riskTier',
          'certifications',
          'annualSpend',
          'insuranceExpiry',
          'contractRenewal',
          'taxIdOnFile',
          'owner',
          'lastReviewDate',
        ],
        ColumnPinning: {review: 'left'},
        ColumnSizing: {
          certifications: {Width: 280},
        },
        ColumnSorts: [{ColumnId: 'vendorName', SortOrder: 'Asc'}],
      },
      {
        Name: 'Pending Review',
        AutoSizeColumns: true,
        TableColumns: [
          'review',
          'vendorName',
          'category',
          'status',
          'riskTier',
          'certifications',
          'insuranceExpiry',
          'taxIdOnFile',
          'owner',
        ],
        ColumnPinning: {review: 'left'},
        ColumnSizing: {
          certifications: {Width: 280},
        },
        GridFilter: {
          Expression: 'QUERY("Pending Review")',
        },
      },
      {
        Name: 'By Category',
        AutoSizeColumns: true,
        SuppressAggFuncInHeader: true,
        RowGroupedColumns: ['category'],
        RowGroupValues: {
          RowGroupDefaultBehavior: 'expanded',
        },
        ColumnPinning: {
          'ag-Grid-AutoColumn': 'left',
          review: 'left',
        },
        TableAggregationColumns: [
          {ColumnId: 'annualSpend', AggFunc: 'sum'},
          {ColumnId: 'id', AggFunc: 'count'},
        ],
        TableColumns: [
          'ag-Grid-AutoColumn',
          'review',
          'vendorName',
          'status',
          'riskTier',
          'certifications',
          'annualSpend',
          'insuranceExpiry',
          'owner',
        ],
        ColumnSizing: {
          certifications: {Width: 260},
        },
      },
    ],
  },
  Alert: {
    AlertDefinitions: [
      {
        Name: 'New vendor queued',
        MessageType: 'Info',
        MessageHeader: 'New vendor',
        MessageText:
          'A vendor was added to the registry — review in Pending Review',
        Scope: {All: true},
        Rule: 'ROW_ADDED{}',
        AlertProperties: {
          DisplayNotification: true,
          HighlightRow: {
            BackColor: 'rgba(91, 155, 213, 0.35)',
          },
        },
      },
    ],
  },
};

const aliceComplianceFormats: InitialState['FormatColumn'] = {
  FormatColumns: [
    {
      Name: 'shared-format-status-pending',
      Scope: {ColumnIds: ['status']},
      Rule: '[status] = "Pending"',
      Style: {
        BackColor: 'var(--ab-color-palette-3)',
        ForeColor: 'var(--ab-color-palette-4)',
        FontWeight: 'Bold',
      },
    },
    {
      Name: 'shared-format-status-suspended',
      Scope: {ColumnIds: ['status']},
      Rule: '[status] = "Suspended"',
      Style: {
        BackColor: 'var(--ab-color-palette-1)',
        ForeColor: 'var(--ab-color-palette-2)',
        FontWeight: 'Bold',
      },
    },
    {
      Name: 'shared-format-high-risk',
      Scope: {ColumnIds: ['riskTier']},
      Rule: '[riskTier] = "High"',
      Style: {
        ForeColor: 'var(--ab-color-destructive)',
        FontWeight: 'Bold',
      },
    },
    {
      Name: 'format-spend',
      Scope: {ColumnIds: ['annualSpend']},
      DisplayFormat: 'Dollar',
    },
    {
      Name: 'format-dates',
      Scope: {
        ColumnIds: ['insuranceExpiry', 'contractRenewal', 'lastReviewDate'],
      },
      DisplayFormat: {
        Formatter: 'DateFormatter',
        Options: {Pattern: 'dd MMM yyyy'},
      },
    },
  ],
};

const bobLocalFormats: InitialState['FormatColumn'] = {
  FormatColumns: [
    {
      Name: 'format-spend',
      Scope: {ColumnIds: ['annualSpend']},
      DisplayFormat: 'Dollar',
    },
    {
      Name: 'format-dates',
      Scope: {
        ColumnIds: ['insuranceExpiry', 'contractRenewal', 'lastReviewDate'],
      },
      DisplayFormat: {
        Formatter: 'DateFormatter',
        Options: {Pattern: 'dd MMM yyyy'},
      },
    },
  ],
};

const aliceVendorToolbarButtons: AdaptableButton<CustomToolbarButtonContext>[] =
  [
    {
      label: 'Share formats',
      buttonStyle: {tone: 'success', variant: 'raised'},
      onClick: async (_button, context) => {
        await shareComplianceFormats(context.adaptableApi);
      },
    },
    {
      label: 'Import pending batch',
      buttonStyle: {tone: 'neutral', variant: 'outlined'},
      onClick: async (_button, context) => {
        try {
          await navigator.clipboard.writeText(PENDING_IMPORT_CSV);
        } catch {
          // clipboard may be blocked in some browsers
        }
        context.adaptableApi.dataImportApi.openImportWizard();
      },
    },
  ];

const bobVendorToolbarButtons: AdaptableButton<CustomToolbarButtonContext>[] = [
  {
    label: 'Import formats',
    buttonStyle: {tone: 'success', variant: 'raised'},
    onClick: async (_button, context) => {
      await importSharedComplianceFormats(context.adaptableApi);
    },
  },
];

const SUPPLIER_REGISTRY_STATE_KEY_PREFIX = 'SupplierRegistryShowcase_';

function resetDemoState(context: DashboardButtonContext) {
  localStorage.removeItem(COMMENTS_PERSISTENCE_KEY);
  localStorage.removeItem(TEAM_SHARING_STATE_KEY);
  ['Alice', 'Bob'].forEach(user => {
    localStorage.removeItem(`${SUPPLIER_REGISTRY_STATE_KEY_PREFIX}${user}`);
  });
  context.adaptableApi.stateApi.reloadInitialState();
}

export const getAdaptableOptionsForUser = (
  userName: 'Alice' | 'Bob',
  updateCurrentUser: (userName: 'Alice' | 'Bob') => void
): AdaptableOptions<Vendor> => {
  const userSpecificInitialState: Partial<InitialState> =
    userName === 'Alice'
      ? {
          Theme: {CurrentTheme: 'light'},
          FormatColumn: aliceComplianceFormats,
        }
      : {
          Theme: {CurrentTheme: 'dark'},
          FormatColumn: bobLocalFormats,
        };

  return {
    primaryKey: 'id',
    adaptableId: 'Showcase: Approved Supplier Registry',
    adaptableStateKey: `${SUPPLIER_REGISTRY_STATE_KEY_PREFIX}${userName}`,
    userName,
    commentOptions: {
      loadCommentThreads: async (commentLoadContext: CommentLoadContext) => {
        commentsService.subscribeToComments(comments => {
          commentLoadContext.adaptableApi.commentApi.setComments(comments);
        });
        return commentsService.getComments();
      },
      persistCommentThreads: async (commentThreads: CommentThread[]) => {
        commentsService.setComments(commentThreads);
      },
    },
    teamSharingOptions: {
      enableTeamSharing: true,
      loadSharedEntities: async () => {
        const data = localStorage.getItem(TEAM_SHARING_STATE_KEY);
        if (!data) {
          return [];
        }
        try {
          return JSON.parse(data);
        } catch {
          return [];
        }
      },
      persistSharedEntities: entries => {
        localStorage.setItem(TEAM_SHARING_STATE_KEY, JSON.stringify(entries));
        return Promise.resolve();
      },
      updateNotification: 'AlertWithNotification',
      suppressOverrideConfigWarning: true,
      updateInterval: 0.25,
    },
    rowFormOptions: {
      autoHandle: true,
      setPrimaryKeyValue: context => {
        const rows = context.adaptableApi.gridApi.getGridData() as Vendor[];
        const nextId = rows.reduce((max, row) => Math.max(max, row.id), 0) + 1;
        return {
          ...context.rowData,
          id: nextId,
          status: context.rowData.status ?? 'Pending',
          owner: userName,
          lastReviewDate: new Date().toISOString().slice(0, 10),
          taxIdOnFile: context.rowData.taxIdOnFile ?? false,
          certifications: context.rowData.certifications ?? ['GDPR'],
        };
      },
      includeColumnInRowForm: (rowFormColumnContext: RowFormColumnContext) => {
        const column: AdaptableColumn = rowFormColumnContext.adaptableColumn;
        return column.columnId !== 'id' && column.columnId !== 'certifications';
      },
    },
    actionColumnOptions: {
      actionColumns: [
        {
          columnId: 'review',
          friendlyName: 'Review',
          settings: {
            width: 72,
            suppressMenu: true,
            resizable: false,
            suppressMovable: true,
          },
          controls: [
            {
              type: 'command',
              command: 'edit',
              label: 'Edit',
            },
          ],
        },
      ],
    },
    dashboardOptions: {
      customToolbars: [
        {
          name: 'CurrentUser',
          title: 'Team',
          toolbarButtons: [
            {
              label: 'Alice',
              buttonStyle: () => ({
                tone: userName === 'Alice' ? 'success' : 'neutral',
                variant: userName === 'Alice' ? 'raised' : 'outlined',
              }),
              onClick: () => updateCurrentUser('Alice'),
            },
            {
              label: 'Bob',
              buttonStyle: () => ({
                tone: userName === 'Bob' ? 'success' : 'neutral',
                variant: userName === 'Bob' ? 'raised' : 'outlined',
              }),
              onClick: () => updateCurrentUser('Bob'),
            },
          ],
        },
        {
          name: 'VendorActions',
          title: 'Vendors',
          toolbarButtons: [
            {
              label: 'Add vendor',
              buttonStyle: {tone: 'info', variant: 'raised'},
              onClick: (
                _button: AdaptableButton<CustomToolbarButtonContext>,
                context: CustomToolbarButtonContext
              ) => {
                context.adaptableApi.rowFormApi.displayCreateRowForm();
              },
            },
            ...(userName === 'Alice'
              ? aliceVendorToolbarButtons
              : bobVendorToolbarButtons),
          ],
        },
      ],
      customDashboardButtons: [
        {
          tooltip: 'Reset demo state',
          icon: {name: 'refresh'},
          buttonStyle: {tone: 'error'},
          onClick: (_button, context) => resetDemoState(context),
        },
      ],
    },
    initialState: {
      ...commonInitialState,
      ...userSpecificInitialState,
    },
  };
};
```

**`src/App.tsx`**

```ts
import * as React from 'react';
import {useState} from 'react';
import {
  Adaptable,
  AdaptableApi,
  AdaptableReadyInfo,
} from '@adaptabletools/adaptable-react-aggrid';

import '@adaptabletools/adaptable-react-aggrid/index.css';
import './styles.css';

import {gridOptions} from './gridOptions';
import {agGridModules} from './agGridModules';
import {getAdaptableOptionsForUser} from './adaptableOptions';
import {onAdaptableReady} from './onAdaptableReady';

const Grid = ({
  currentUser,
  onCurrentUserChange,
}: {
  currentUser: 'Alice' | 'Bob';
  onCurrentUserChange: (userName: 'Alice' | 'Bob') => void;
}) => {
  const adaptableApiRef = React.useRef<AdaptableApi>(null);

  const adaptableOptions = getAdaptableOptionsForUser(
    currentUser,
    onCurrentUserChange
  );

  const agGridOptions = {...gridOptions};

  return (
    <div style={{display: 'flex', flexFlow: 'column', height: '100vh'}}>
      <Adaptable.Provider
        gridOptions={agGridOptions}
        adaptableOptions={adaptableOptions}
        modules={[...agGridModules]}
        onAdaptableReady={(adaptableReadyInfo: AdaptableReadyInfo) => {
          adaptableApiRef.current = adaptableReadyInfo.adaptableApi;
          onAdaptableReady(adaptableReadyInfo);
        }}>
        <Adaptable.UI key={`${currentUser}-ui`} style={{flex: 'none'}} />
        <Adaptable.AgGridReact />
      </Adaptable.Provider>
    </div>
  );
};

const App: React.FunctionComponent = () => {
  const [currentUser, setCurrentUser] = useState<'Alice' | 'Bob'>('Alice');

  return (
    <Grid
      key={currentUser}
      currentUser={currentUser}
      onCurrentUserChange={userName => setCurrentUser(userName)}
    />
  );
};

export default App;
```

**`src/teamSharingHelpers.ts`**

```ts
import {AdaptableApi, AdaptableSharedEntity, FormatColumn} from '@adaptabletools/adaptable-react-aggrid';

/** Format columns Alice shares with the compliance team via Team Sharing */
export const SHARED_FORMAT_COLUMN_NAMES = [
  'shared-format-status-pending',
  'shared-format-status-suspended',
  'shared-format-high-risk',
] as const;

/**
 * Alice: upload compliance format columns to the team share using `teamSharingApi`.
 */
export async function shareComplianceFormats(
  adaptableApi: AdaptableApi
): Promise<number> {
  await adaptableApi.teamSharingApi.loadSharedEntities();

  let sharedCount = 0;
  SHARED_FORMAT_COLUMN_NAMES.forEach(name => {
    const formatColumn = adaptableApi.formatColumnApi.getFormatColumnByName(name);
    if (!formatColumn) {
      return;
    }

    const alreadyShared = adaptableApi.teamSharingApi
      .getLoadedAdaptableSharedEntities()
      .some(
        entity =>
          entity.Module === 'FormatColumn' &&
          (entity.Entity as FormatColumn).Name === name
      );

    if (alreadyShared) {
      return;
    }

    adaptableApi.teamSharingApi.shareAdaptableEntity(
      formatColumn,
      'FormatColumn',
      {
        description: 'Alice: Compliance highlights for vendor review',
        type: 'Active',
      }
    );
    sharedCount += 1;
  });

  return sharedCount;
}

/**
 * Bob: download shared format columns from the team share using `teamSharingApi`.
 */
export async function importSharedComplianceFormats(
  adaptableApi: AdaptableApi
): Promise<number> {
  await adaptableApi.teamSharingApi.loadSharedEntities();

  const formatShares = adaptableApi.teamSharingApi
    .getLoadedAdaptableSharedEntities()
    .filter(
      (entity: AdaptableSharedEntity) =>
        entity.Module === 'FormatColumn' && entity.Type === 'Active'
    );

  let importedCount = 0;
  formatShares.forEach(sharedEntity => {
    const entityName = (sharedEntity.Entity as FormatColumn | undefined)?.Name;
    if (!entityName) {
      return;
    }
    if (adaptableApi.formatColumnApi.getFormatColumnByName(entityName)) {
      return;
    }

    adaptableApi.teamSharingApi.importSharedEntry(sharedEntity);
    importedCount += 1;
  });

  return importedCount;
}
```

**`src/onAdaptableReady.ts`**

```ts
import {AdaptableReadyInfo} from '@adaptabletools/adaptable-react-aggrid';

/** No startup side-effects — team sharing is driven from toolbar buttons. */
export const onAdaptableReady = (_adaptableReadyInfo: AdaptableReadyInfo) => {};
```

**`src/commentsSeed.ts`**

```ts
import {CommentThread} from '@adaptabletools/adaptable-react-aggrid';

import {COMMENT_THREAD_VENDOR_IDS} from './rowData';

export function getSeedComments(): CommentThread[] {
  const day = 86_400_000;
  const now = Date.now();

  return [
    {
      ColumnId: 'insuranceExpiry',
      PrimaryKeyValue: COMMENT_THREAD_VENDOR_IDS.insuranceReview,
      Uuid: 'seed-thread-insurance-7',
      Comments: [
        {
          Uuid: 'seed-comment-alice-insurance',
          Value:
            'Imported from procurement — please confirm certificate of insurance.',
          Author: {UserName: 'Alice'},
          Timestamp: now - 2 * day,
        },
        {
          Uuid: 'seed-comment-bob-insurance',
          Value: 'COI still missing on file. Hold approval until received.',
          Author: {UserName: 'Bob'},
          Timestamp: now - day,
        },
      ],
    },
    {
      ColumnId: 'status',
      PrimaryKeyValue: COMMENT_THREAD_VENDOR_IDS.statusReview,
      Uuid: 'seed-thread-status-12',
      Comments: [
        {
          Uuid: 'seed-comment-alice-status',
          Value:
            'New applicant — W-9 received, routing for compliance review.',
          Author: {UserName: 'Alice'},
          Timestamp: now - 3 * day,
        },
        {
          Uuid: 'seed-comment-bob-status',
          Value:
            'Tax ID validated. OK to approve once insurance is confirmed.',
          Author: {UserName: 'Bob'},
          Timestamp: now - 12 * 3_600_000,
        },
      ],
    },
  ];
}
```

**`src/importSample.ts`**

```ts
/** Paste into the Data Import wizard (Add Rows) as Alice */
export const PENDING_IMPORT_CSV = `id,vendorName,category,status,riskTier,annualSpend,insuranceExpiry,contractRenewal,taxIdOnFile,owner,lastReviewDate
101,Rainbow Catering,Facilities,Pending,Medium,45000,2026-03-15,2026-12-31,true,Alice,2026-05-01
102,TechBridge IT,IT,Pending,Low,120000,2026-09-01,2027-06-30,true,Alice,2026-05-01
103,Summit Logistics,Logistics,Pending,High,88000,2026-04-30,2027-04-30,false,Alice,2026-05-01
104,Bright Signs Co,Marketing,Pending,Low,22000,2026-01-20,2026-12-31,true,Alice,2026-05-01`;
```

**`src/rowData.ts`**

```ts
export type VendorStatus = 'Approved' | 'Pending' | 'Suspended';
export type RiskTier = 'Low' | 'Medium' | 'High';
export type VendorCategory =
  | 'IT'
  | 'Facilities'
  | 'Marketing'
  | 'Logistics'
  | 'Professional Services';

export interface Vendor {
  id: number;
  vendorName: string;
  category: VendorCategory;
  status: VendorStatus;
  riskTier: RiskTier;
  /** Compliance attestations on file — `textArray` badge column */
  certifications: string[];
  annualSpend: number;
  insuranceExpiry: string;
  contractRenewal: string;
  taxIdOnFile: boolean;
  owner: string;
  lastReviewDate: string;
}

function pseudoRandom(seed: number): number {
  const x = Math.sin(seed * 7919) * 10000;
  return x - Math.floor(x);
}

function certificationsForVendor(
  id: number,
  category: VendorCategory,
  riskTier: RiskTier
): string[] {
  const certs = new Set<string>(['GDPR']);

  if (category === 'IT' || category === 'Professional Services') {
    certs.add('SOC 2');
    certs.add('ISO 27001');
    if (pseudoRandom(id * 13) > 0.45) {
      certs.add('DPA');
    }
  }
  if (category === 'Logistics' && pseudoRandom(id * 17) > 0.35) {
    certs.add('PCI DSS');
  }
  if (category === 'Marketing' && pseudoRandom(id * 19) > 0.5) {
    certs.add('DPA');
  }
  if (riskTier === 'High' && pseudoRandom(id * 23) > 0.55) {
    certs.add('SOC 2');
  }
  if (category === 'Facilities' && pseudoRandom(id * 29) > 0.7) {
    certs.add('ISO 45001');
  }

  return Array.from(certs);
}

const VENDORS: Omit<Vendor, 'id' | 'certifications'>[] = [
  {
    vendorName: 'CloudNine Hosting',
    category: 'IT',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 185000,
    insuranceExpiry: '2026-11-30',
    contractRenewal: '2027-06-30',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-01-15',
  },
  {
    vendorName: 'Harbor Cleaning Services',
    category: 'Facilities',
    status: 'Pending',
    riskTier: 'Medium',
    annualSpend: 42000,
    insuranceExpiry: '2025-06-15',
    contractRenewal: '2026-12-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-04-20',
  },
  {
    vendorName: 'Brightline Creative',
    category: 'Marketing',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 96000,
    insuranceExpiry: '2026-08-01',
    contractRenewal: '2026-12-31',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2025-12-10',
  },
  {
    vendorName: 'Summit Freight',
    category: 'Logistics',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 210000,
    insuranceExpiry: '2026-05-20',
    contractRenewal: '2027-03-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-02-28',
  },
  {
    vendorName: 'Atlas Legal Advisory',
    category: 'Professional Services',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 145000,
    insuranceExpiry: '2027-01-31',
    contractRenewal: '2027-12-31',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-03-01',
  },
  {
    vendorName: 'Vertex Security Systems',
    category: 'Facilities',
    status: 'Suspended',
    riskTier: 'High',
    annualSpend: 78000,
    insuranceExpiry: '2025-03-01',
    contractRenewal: '2025-12-31',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2025-11-05',
  },
  {
    vendorName: 'Pacific Office Supplies',
    category: 'Facilities',
    status: 'Pending',
    riskTier: 'Low',
    annualSpend: 28000,
    insuranceExpiry: '2025-07-10',
    contractRenewal: '2026-09-30',
    taxIdOnFile: false,
    owner: 'Alice',
    lastReviewDate: '2026-04-18',
  },
  {
    vendorName: 'Lumen Analytics',
    category: 'IT',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 320000,
    insuranceExpiry: '2026-10-15',
    contractRenewal: '2028-03-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-01-22',
  },
  {
    vendorName: 'Orion Print Group',
    category: 'Marketing',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 54000,
    insuranceExpiry: '2026-04-30',
    contractRenewal: '2027-04-30',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2025-10-14',
  },
  {
    vendorName: 'Cascade Couriers',
    category: 'Logistics',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 118000,
    insuranceExpiry: '2026-07-01',
    contractRenewal: '2027-07-01',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-03-12',
  },
  {
    vendorName: 'Sterling Audit Partners',
    category: 'Professional Services',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 89000,
    insuranceExpiry: '2027-02-28',
    contractRenewal: '2027-12-31',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-02-05',
  },
  {
    vendorName: 'Northwind Stationery',
    category: 'Facilities',
    status: 'Pending',
    riskTier: 'Low',
    annualSpend: 19000,
    insuranceExpiry: '2026-12-01',
    contractRenewal: '2026-12-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-04-22',
  },
  {
    vendorName: 'Nova DevTools',
    category: 'IT',
    status: 'Approved',
    riskTier: 'High',
    annualSpend: 275000,
    insuranceExpiry: '2026-09-30',
    contractRenewal: '2027-09-30',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-01-08',
  },
  {
    vendorName: 'Helios Catering',
    category: 'Facilities',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 36000,
    insuranceExpiry: '2026-06-30',
    contractRenewal: '2026-12-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2025-09-20',
  },
  {
    vendorName: 'Pioneer Events',
    category: 'Marketing',
    status: 'Suspended',
    riskTier: 'Medium',
    annualSpend: 67000,
    insuranceExpiry: '2025-05-01',
    contractRenewal: '2025-11-30',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2025-08-30',
  },
  {
    vendorName: 'BluePeak Warehousing',
    category: 'Logistics',
    status: 'Approved',
    riskTier: 'High',
    annualSpend: 156000,
    insuranceExpiry: '2026-03-31',
    contractRenewal: '2027-03-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-03-30',
  },
  {
    vendorName: 'Redwood Consulting',
    category: 'Professional Services',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 112000,
    insuranceExpiry: '2026-11-15',
    contractRenewal: '2027-11-15',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-02-18',
  },
  {
    vendorName: 'TechBridge Support',
    category: 'IT',
    status: 'Pending',
    riskTier: 'Medium',
    annualSpend: 134000,
    insuranceExpiry: '2026-01-20',
    contractRenewal: '2027-01-20',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-04-25',
  },
  {
    vendorName: 'Greenfield Landscaping',
    category: 'Facilities',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 24000,
    insuranceExpiry: '2026-05-15',
    contractRenewal: '2026-11-15',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2025-11-22',
  },
  {
    vendorName: 'Signal Media Agency',
    category: 'Marketing',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 128000,
    insuranceExpiry: '2026-08-30',
    contractRenewal: '2027-08-30',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-01-30',
  },
  {
    vendorName: 'MetroLine Haulage',
    category: 'Logistics',
    status: 'Pending',
    riskTier: 'High',
    annualSpend: 94000,
    insuranceExpiry: '2025-08-01',
    contractRenewal: '2026-08-01',
    taxIdOnFile: false,
    owner: 'Bob',
    lastReviewDate: '2026-04-10',
  },
  {
    vendorName: 'Clarity Tax Advisors',
    category: 'Professional Services',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 52000,
    insuranceExpiry: '2027-04-01',
    contractRenewal: '2027-12-31',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-03-05',
  },
  {
    vendorName: 'Apex Software Licenses',
    category: 'IT',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 410000,
    insuranceExpiry: '2026-12-31',
    contractRenewal: '2027-12-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-02-12',
  },
  {
    vendorName: 'Urban Facilities Mgmt',
    category: 'Facilities',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 198000,
    insuranceExpiry: '2026-02-28',
    contractRenewal: '2027-02-28',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-01-05',
  },
  {
    vendorName: 'Spark Digital',
    category: 'Marketing',
    status: 'Pending',
    riskTier: 'Low',
    annualSpend: 47000,
    insuranceExpiry: '2026-10-01',
    contractRenewal: '2027-10-01',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-04-28',
  },
  {
    vendorName: 'CrossCountry Freight',
    category: 'Logistics',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 167000,
    insuranceExpiry: '2026-06-15',
    contractRenewal: '2027-06-15',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2025-12-18',
  },
  {
    vendorName: 'Harbor HR Consulting',
    category: 'Professional Services',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 73000,
    insuranceExpiry: '2026-09-01',
    contractRenewal: '2027-09-01',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-02-25',
  },
  {
    vendorName: 'SecureNet MSP',
    category: 'IT',
    status: 'Suspended',
    riskTier: 'High',
    annualSpend: 221000,
    insuranceExpiry: '2025-04-15',
    contractRenewal: '2025-10-15',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2025-07-12',
  },
  {
    vendorName: 'FreshServe Catering',
    category: 'Facilities',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 31000,
    insuranceExpiry: '2026-07-20',
    contractRenewal: '2026-12-31',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2025-10-30',
  },
  {
    vendorName: 'Momentum PR',
    category: 'Marketing',
    status: 'Approved',
    riskTier: 'Medium',
    annualSpend: 86000,
    insuranceExpiry: '2026-03-15',
    contractRenewal: '2027-03-15',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-03-18',
  },
  {
    vendorName: 'RapidRoute Logistics',
    category: 'Logistics',
    status: 'Approved',
    riskTier: 'Low',
    annualSpend: 102000,
    insuranceExpiry: '2026-11-01',
    contractRenewal: '2027-11-01',
    taxIdOnFile: true,
    owner: 'Alice',
    lastReviewDate: '2026-04-01',
  },
  {
    vendorName: 'Insight Strategy Group',
    category: 'Professional Services',
    status: 'Pending',
    riskTier: 'Medium',
    annualSpend: 125000,
    insuranceExpiry: '2026-05-01',
    contractRenewal: '2027-05-01',
    taxIdOnFile: true,
    owner: 'Bob',
    lastReviewDate: '2026-04-15',
  },
];

export const rowData: Vendor[] = VENDORS.map((vendor, index) => {
  const id = index + 1;
  return {
    id,
    ...vendor,
    certifications: certificationsForVendor(id, vendor.category, vendor.riskTier),
  };
});

/** id 7 = Pacific Office Supplies, id 12 = Northwind Stationery — used for seeded comment threads */
export const COMMENT_THREAD_VENDOR_IDS = {
  insuranceReview: 7,
  statusReview: 12,
} as const;
```

**`src/columnDefs.ts`**

```ts
import {ColDef} from 'ag-grid-enterprise';

export const columnDefs: ColDef[] = [
  {
    field: 'vendorName',
    headerName: 'Vendor',
    cellDataType: 'text',
    width: 200,
  },
  {
    field: 'category',
    cellDataType: 'text',
    enableRowGroup: true,
    width: 150,
  },
  {
    field: 'status',
    cellDataType: 'text',
    enableRowGroup: true,
    width: 110,
  },
  {
    field: 'riskTier',
    headerName: 'Risk',
    cellDataType: 'text',
    width: 90,
  },
  {
    field: 'certifications',
    headerName: 'Certifications',
    cellDataType: 'textArray',
    autoHeight: true,
    width: 280,
  },
  {
    field: 'annualSpend',
    headerName: 'Annual Spend',
    cellDataType: 'number',
    type: 'rightAligned',
    enableValue: true,
    width: 130,
  },
  {
    field: 'insuranceExpiry',
    headerName: 'Insurance Expiry',
    cellDataType: 'date',
    width: 140,
  },
  {
    field: 'contractRenewal',
    headerName: 'Contract Renewal',
    cellDataType: 'date',
    width: 140,
  },
  {
    field: 'taxIdOnFile',
    headerName: 'W-9 On File',
    cellDataType: 'boolean',
    width: 110,
  },
  {
    field: 'owner',
    cellDataType: 'text',
    width: 90,
  },
  {
    field: 'lastReviewDate',
    headerName: 'Last Review',
    cellDataType: 'date',
    width: 120,
  },
  {field: 'id', hide: true, editable: false},
];
```

**`src/gridOptions.ts`**

```ts
import {GridOptions, themeBalham} from 'ag-grid-enterprise';

import {columnDefs} from './columnDefs';
import {Vendor, rowData as supplierRowData} from './rowData';

export const gridOptions: GridOptions<Vendor> = {
  theme: themeBalham,
  defaultColDef: {
    resizable: true,
    sortable: true,
    editable: true,
    filter: true,
    floatingFilter: true,
  },
  columnDefs: columnDefs,
  rowData: supplierRowData,
  sideBar: true,
  suppressMenuHide: true,
  columnMenu: 'new',
  cellSelection: true,
  statusBar: {
    statusPanels: [
      {statusPanel: 'agTotalRowCountComponent', align: 'left'},
      {statusPanel: 'agFilteredRowCountComponent'},
      {
        key: 'Center Panel',
        statusPanel: 'AdaptableStatusPanel',
        align: 'center',
      },
    ],
  },
};
```

**`src/styles.css`**

```css
/* Procurement (Alice, light) and compliance (Bob, dark) brand tints on shipped themes.
   Shared format columns use palette CSS variables so highlights follow the active theme. */

:root.ab--theme-light {
  --ab-color-accent: #0d6e6e;

  /* Pending review — palette 3/4 */
  --ab-color-palette-3: #e4f5f5;
  --ab-color-palette-4: #0a5c5c;

  /* Suspended — palette 1/2 */
  --ab-color-palette-1: #fdecee;
  --ab-color-palette-2: #8b2942;
}

:root.ab--theme-dark {
  --ab-color-accent: #6eb5e8;
  /* Dashboard header title + icon buttons use accent / accent-foreground */
  --ab-color-accent-foreground: #152a3d;

  /* Pending review — palette 3/4 */
  --ab-color-palette-3: #2a3548;
  --ab-color-palette-4: #c9dcf0;

  /* Suspended — palette 1/2 */
  --ab-color-palette-1: #3d2430;
  --ab-color-palette-2: #e8a4b8;

  /* High-risk format uses --ab-color-destructive */
  --ab-color-destructive: #f08090;
}

/* Module shortcut icons (tone none) inherit header colour; leave error-toned buttons (reset) red */
:root.ab--theme-dark .ab-Dashboard__header .ab-SimpleButton--tone-none,
:root.ab--theme-dark
  .ab-Dashboard__header
  .ab-SimpleButton--tone-none
  .ab-Icon,
:root.ab--theme-dark .ab-Dashboard__header .ab-Dashboard__application-icon {
  color: var(--ab-dashboard-header__color);
}

:root.ab--theme-dark .ab-Dashboard__header .ab-SimpleButton--tone-error,
:root.ab--theme-dark
  .ab-Dashboard__header
  .ab-SimpleButton--tone-error
  .ab-Icon {
  color: var(--ab-color-destructive);
}
```

### How the Demo Works

The **Share formats** and **Import formats** toolbar buttons are thin wrappers around `adaptableApi.teamSharingApi` — the same API you would call from your own dashboard buttons or workflow code.

Logic lives in `teamSharingHelpers.ts` in the sandpack (hidden file). 

The flow is:

**Alice — share**

1. `teamSharingApi.loadSharedEntities()` — refresh the local catalogue from your `loadSharedEntities` handler
2. `formatColumnApi.getFormatColumnByName(...)` — resolve each compliance format column in Alice’s state
3. `teamSharingApi.shareAdaptableEntity(formatColumn, 'FormatColumn', { type: 'Active', ... })` — upload as an **Active** share so colleagues receive updates

**Bob — import**

1. `teamSharingApi.loadSharedEntities()` — fetch Alice’s uploads
2. `teamSharingApi.getLoadedAdaptableSharedEntities()` — list available shares
3. `teamSharingApi.importSharedEntry(sharedEntity)` — merge each `FormatColumn` Bob does not already have

In production, `loadSharedEntities` / `persistSharedEntities` would call your backend rather than the demo’s `localStorage` store — see [Team Sharing](https://www.adaptabletools.com/docs/handbook-team-sharing/index.md) and [Custom Team Sharing](https://www.adaptabletools.com/docs/handbook-team-sharing-custom/index.md).

- **Import pending batch** uses a different API: `dataImportApi.openImportWizard()` (with sample CSV copied via the clipboard)
- **Add vendor** uses `rowFormApi.displayCreateRowForm()`
