ChemPal Documentation - v0.0.13-beta.5
    Preparing search index...
    • Defines the column configuration for the product results table. Each column declares its accessor, cell renderer, sort/filter functions, and (optionally) meta.drawer to opt into a pre-search drawer accordion.

      The returned order is the render order in both the table header and the drawer's column-backed sections (see DrawerSearchPanel).

      Returns ColumnDef<Product, unknown>[]

      Ordered TanStack column definitions for the Product row type.

      const columns = TableColumns();
      useReactTable({ columns, data, ... });
      // columns.map(c => c.id) β†’
      // ["expander", "title", "supplier", "country", "shipping",
      // "availability", "description", "price", "quantity", "uom"]
      // columns.filter(c => c.meta?.drawer).map(c => c.id) β†’
      // ["supplier", "country", "shipping", "availability", "price"]
      export default function TableColumns(): ColumnDef<Product, unknown>[] {
      return [
      {
      id: "expander",
      header: () => null,
      cell: ({ row }: ProductRow) => {
      if (!row?.original?.variants || row.original.variants.length === 0) {
      return;
      }
      return row.getCanExpand() ? (
      <button
      {...{
      onClick: row.getToggleExpandedHandler(),
      style: { cursor: "pointer" },
      }}
      className={styles["svg-button-icon"]}
      >
      {row.getIsExpanded() ? (
      <ArrowDropDownIcon fontSize="small" />
      ) : (
      <ArrowRightIcon fontSize="small" />
      )}
      </button>
      ) : null;
      },
      enableHiding: false,
      minSize: 10,
      maxSize: 10,
      size: 10,
      enableSorting: false,
      enableColumnFilter: false,
      enableResizing: false,
      },
      {
      id: "title",
      accessorKey: "title",
      header: "Title",
      cell: ({ row }: ProductRow) => {
      // Indent variant rows so the hierarchy is visually obvious β€” 16px per
      // depth level, matching the chevron column width.
      const indent = row.depth > 0 ? row.depth * 16 : 0;
      return (
      <span style={{ paddingLeft: indent, display: "inline-block" }}>
      <Link
      history={{
      type: "product",
      data: omit(row.original, "variants"),
      }}
      href={row.original.url}
      >
      {row.original.title}
      </Link>
      </span>
      );
      },
      enableHiding: false,
      filterFn: "includeHierarchy",
      meta: {
      filterPlaceholder: "Title...",
      filterVariant: "text",
      style: {
      textAlign: "left",
      },
      },
      },
      {
      id: "supplier",
      header: "Supplier",
      accessorKey: "supplier",
      cell: (info) => info.getValue(),
      filterFn: "multiSelect",
      meta: {
      filterPlaceholder: "BVV, HiMedia, etc...",
      filterVariant: "select",
      style: {
      textAlign: "left",
      },
      drawer: {
      label: "Search Suppliers",
      widget: "autocompleteStrings",
      options: SupplierFactory.supplierList(),
      optionLabels: SupplierFactory.supplierDisplayNames(),
      emptyHelperText: "All suppliers included by default",
      placeholder: "Type supplier name",
      bind: { kind: "selectedSuppliers" },
      },
      },
      },
      {
      id: "country",
      header: "Country",
      accessorKey: "supplierCountry",
      cell: ({ row }: ProductRow) => {
      const country = row.original.supplierCountry;
      if (!country) return null;
      if (!hasFlag(country)) return country;
      const countryName = isLocationCode(country) ? locations[country].name : country;
      return (
      <CountryFlagTooltip title={countryName} placement="top">
      <span>{getUnicodeFlagIcon(country)}</span>
      </CountryFlagTooltip>
      );
      },
      filterFn: "multiSelect",
      meta: {
      filterPlaceholder: "πŸ‡ΊπŸ‡Έ πŸ‡¨πŸ‡³ …",
      filterVariant: "select",
      style: {
      textAlign: "center",
      },
      renderSelectOption: (code) => (hasFlag(code) ? getUnicodeFlagIcon(code) : code),
      drawer: {
      label: "Country",
      widget: "autocompleteObjects",
      options: SUPPLIER_COUNTRY_OPTIONS,
      emptyHelperText: "All countries included by default",
      placeholder: "Type country or code",
      bind: { kind: "searchFilters", key: "country" },
      },
      },
      },
      {
      id: "shipping",
      header: "Shipping",
      accessorKey: "supplierShipping",
      cell: (info) => info.getValue(),
      filterFn: "multiSelect",
      meta: {
      filterPlaceholder: "Shipping...",
      filterVariant: "select",
      drawer: {
      label: "Shipping Type",
      widget: "chips",
      options: SHIPPING_OPTIONS,
      formatChipLabel: (option) => option.charAt(0).toUpperCase() + option.slice(1),
      bind: { kind: "searchFilters", key: "shippingType" },
      },
      },
      },
      {
      id: "availability",
      header: "Availability",
      accessorKey: "availability",
      cell: (info) => info.getValue(),
      filterFn: "multiSelect",
      meta: {
      filterPlaceholder: "Availability...",
      filterVariant: "select",
      style: {
      textAlign: "left",
      },
      drawer: {
      label: "Availability",
      widget: "chips",
      options: AVAILABILITY_OPTIONS,
      bind: { kind: "searchFilters", key: "availability" },
      },
      },
      },
      {
      accessorKey: "description",
      header: "Description",
      meta: {
      filterPlaceholder: "Description...",
      filterVariant: "text",
      style: {
      textAlign: "left",
      },
      },
      },
      {
      id: "price",
      header: "Price",
      accessorKey: "price",
      cell: ({ row, table }: CellContext<Product, unknown>) => {
      const { usdPrice, price: rawPrice, currencyCode } = row.original;
      // Read userSettings from table meta rather than context so
      // TableColumns() stays hook-free β€” it's called from both React
      // renders and non-render code paths (getColumnFilterConfig,
      // DrawerSearchPanel's useMemo), and calling useAppContext() from
      // those paths would violate the Rules of Hooks.
      const userSettings = table.options.meta?.userSettings;
      const currency = userSettings?.currency ?? "USD";
      const currencyRate = userSettings?.currencyRate ?? 1;

      // Non-USD product without a USD anchor: we can't convert into the
      // user's chosen currency, so render the native price as-is.
      if (currencyCode !== "USD" && usdPrice === undefined) {
      console.error("Non-USD product is missing USD price", { row });
      const fallbackCurrency = currencyCode ?? "USD";
      return new Intl.NumberFormat(fallbackCurrency, {
      style: "currency",
      currency: fallbackCurrency,
      }).format(Number(rawPrice));
      }

      const priceInUsd = usdPrice ?? Number(rawPrice);

      return new Intl.NumberFormat(currency, {
      style: "currency",
      currency,
      }).format(priceInUsd * currencyRate);
      },
      sortingFn: "priceSortingFn",
      filterFn: "inNumberRangeHierarchy",
      meta: {
      filterPlaceholder: "1.00 - 1000.00",
      filterVariant: "range",
      style: {
      textAlign: "left",
      },
      drawer: {
      label: "Price Range",
      widget: "numberRange",
      adornment: "currency",
      bind: {
      kind: "userSettingsRange",
      minKey: "priceMin",
      maxKey: "priceMax",
      },
      },
      },
      },
      {
      id: "quantity",
      header: "Qty",
      accessorKey: "quantity",
      meta: {
      filterPlaceholder: "1 - 100",
      filterVariant: "range",
      style: {
      textAlign: "left",
      },
      },
      cell: ({ row }: ProductRow) => {
      return `${row.original.quantity} ${row.original.uom}`;
      },
      sortingFn: "quantitySortingFn",
      filterFn: "inNumberRangeHierarchy",
      minSize: 50,
      },
      {
      id: "uom",
      header: "Unit",
      accessorKey: "uom",
      filterFn: "multiSelect",
      meta: {
      filterPlaceholder: "Unit",
      filterVariant: "select",
      style: {
      textAlign: "left",
      },
      },
      },
      ];
      }