Repository: metabase/metabase
Pull request #54921: feat(enterprise): add category dropdown editing
Author: login: spalt08
Created: 2025-03-10T17:28:16Z | Merged: 2025-03-11T16:57:58Z


https://github.com/user-attachments/assets/dcb81459-ed36-4d88-ad0a-ce84cd818d60




Diff:
diff --git a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.module.css b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.module.css
new file mode 100644
index 000000000000..d57d830d4aef
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.module.css
@@ -0,0 +1,11 @@
+.input {
+  transition: none;
+  outline: 2px solid var(--mb-base-color-blue-50);
+  border-color: transparent;
+  border-radius: 0.25rem;
+  box-sizing: border-box;
+
+  /* needed to avoid outline truncation for a focused cell */
+  z-index: 5;
+  position: relative;
+}
diff --git a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
index 2bbaef9785db..d3193d94a571 100644
--- a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
+++ b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
@@ -1,87 +1,193 @@
 import type { CellContext } from "@tanstack/react-table";
-import {
-  type ChangeEvent,
-  type KeyboardEvent,
-  useCallback,
-  useState,
-} from "react";
+import { useCallback, useMemo, useState } from "react";
+import { t } from "ttag";
 
-import { Input } from "metabase/ui";
-import type { RowValue, RowValues } from "metabase-types/api";
+import { skipToken, useGetFieldValuesQuery } from "metabase/api";
+import { getFieldOptions } from "metabase/querying/filters/components/FilterValuePicker/utils";
+import {
+  Box,
+  Combobox,
+  Icon,
+  Input,
+  TextInput,
+  useCombobox,
+} from "metabase/ui";
+import type { DatasetColumn, RowValue, RowValues } from "metabase-types/api";
 
-import S from "./TableDataView.module.css";
+import S from "./EditingBodyCell.module.css";
 import type { UpdatedRowCellsHandlerParams } from "./types";
 
 interface EditingBodyCellProps<TRow, TValue> {
+  column: DatasetColumn;
   cellContext: CellContext<TRow, TValue>;
   onCellValueUpdate: (params: UpdatedRowCellsHandlerParams) => void;
   onCellEditCancel: () => void;
 }
 
-export const EditingBodyCell = ({
-  cellContext,
-  onCellValueUpdate,
-  onCellEditCancel,
-}: EditingBodyCellProps<RowValues, RowValue>) => {
+export const EditingBodyCellConditional = (
+  props: EditingBodyCellProps<RowValues, RowValue>,
+) => {
   const {
-    getValue,
-    row: { index: rowIndex },
-    column: { id: columnName },
-  } = cellContext;
-  const initialValue = getValue<RowValue>();
-  const [value, setValue] = useState<RowValue>(initialValue);
-
-  const doCellValueUpdate = useCallback(() => {
-    if (value !== initialValue) {
-      onCellValueUpdate({
-        data: {
-          [columnName]: value,
-        },
-        rowIndex,
-      });
-    }
-
-    onCellEditCancel();
-  }, [
-    columnName,
-    initialValue,
     onCellEditCancel,
     onCellValueUpdate,
-    rowIndex,
-    value,
-  ]);
+    column,
+    cellContext: {
+      getValue,
+      column: { id: columnName },
+      row: { index: rowIndex },
+    },
+  } = props;
 
-  const handleChange = useCallback(
-    (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value),
-    [],
-  );
+  const initialValue = getValue<RowValue>();
 
-  const handleKeyUp = useCallback(
-    (e: KeyboardEvent<HTMLInputElement>) => {
-      if (e.key === "Enter") {
-        doCellValueUpdate();
-      }
-      if (e.key === "Escape") {
-        onCellEditCancel();
+  const doCellValueUpdate = useCallback(
+    (value: RowValue) => {
+      if (value !== initialValue) {
+        onCellValueUpdate({
+          data: { [columnName]: value },
+          rowIndex,
+        });
       }
+
+      // Hide the editing cell after submitting the value
+      onCellEditCancel();
     },
-    [doCellValueUpdate, onCellEditCancel],
+    [columnName, onCellEditCancel, onCellValueUpdate, rowIndex, initialValue],
   );
 
-  const handleFieldBlur = useCallback(() => {
-    doCellValueUpdate();
-  }, [doCellValueUpdate]);
+  switch (column?.semantic_type) {
+    case "type/State":
+    case "type/Country":
+    case "type/Category":
+      return (
+        <EditingBodyCellSelect
+          initialValue={initialValue}
+          datasetColumn={column}
+          onSubmit={doCellValueUpdate}
+          onCancel={onCellEditCancel}
+        />
+      );
+
+    default:
+      return (
+        <EditingBodyCellText
+          initialValue={initialValue}
+          datasetColumn={column}
+          onSubmit={doCellValueUpdate}
+          onCancel={onCellEditCancel}
+        />
+      );
+  }
+};
+
+interface EditingBodyPrimitiveProps {
+  datasetColumn: DatasetColumn;
+  initialValue: RowValue;
+  onSubmit: (value: RowValue) => unknown;
+  onCancel: () => unknown;
+}
 
+export const EditingBodyCellText = ({
+  initialValue,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
   return (
     <Input
-      value={value as any} // TODO [Milestone 2]: fixup this type after adding specific inputs based on data type
+      defaultValue={(initialValue ?? "").toString()}
       className={S.input}
       variant="unstyled"
       size="xs"
       autoFocus
-      onChange={handleChange}
-      onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onKeyUp={event => {
+        if (event.key === "Escape") {
+          onCancel();
+        } else if (event.key === "Enter") {
+          onSubmit(event.currentTarget.value);
+        }
+      }}
+      onBlur={event => {
+        onSubmit(event.currentTarget.value);
+      }}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    datasetColumn.id ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: onCancel,
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <Input
+          value={(initialValue ?? "").toString()}
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          onMouseDown={onCancel}
+          className={S.input}
+          size="xs"
+        />
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder={t`Search the list`}
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty>{t`Loading values...`}</Combobox.Empty>
+          ) : (
+            <Combobox.Option value={search}>
+              {t`Add option:`} <strong>{search}</strong>
+            </Combobox.Option>
+          )}
+        </Combobox.Options>
+      </Combobox.Dropdown>
+    </Combobox>
+  );
+};
diff --git a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.module.css b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.module.css
index a2f8b228fceb..6bb435d4ddff 100644
--- a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.module.css
+++ b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.module.css
@@ -3,13 +3,3 @@
   width: 100%;
   overflow: hidden;
 }
-
-.input {
-  transition: none;
-
-  &:focus-within {
-    outline: 2px solid var(--mb-base-color-blue-50);
-    border-color: transparent;
-    z-index: 5; /* needed to avoid outline truncation for a focused cell */
-  }
-}
diff --git a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.tsx b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.tsx
index ba34170fc24e..bef6aeda2ebe 100644
--- a/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.tsx
+++ b/enterprise/frontend/src/metabase-enterprise/data_editing/tables/TableDataView.tsx
@@ -11,7 +11,7 @@ import {
 import { formatValue } from "metabase/lib/formatting/value";
 import type { Dataset, RowValue, RowValues } from "metabase-types/api";
 
-import { EditingBodyCell } from "./EditingBodyCell";
+import { EditingBodyCellConditional } from "./EditingBodyCell";
 import type { UpdatedRowCellsHandlerParams } from "./types";
 import { useTableEditing } from "./use-table-editing";
 
@@ -47,8 +47,9 @@ export const TableDataView = ({
         formatter: value => formatValue(value, { column }),
         wrap: false,
         editingCell: cellContext => (
-          <EditingBodyCell
+          <EditingBodyCellConditional
             cellContext={cellContext}
+            column={column}
             onCellValueUpdate={onCellValueUpdate}
             onCellEditCancel={onCellEditCancel}
           />
@@ -84,9 +85,13 @@ export const TableDataView = ({
         cellId: string;
       },
     ) => {
-      onCellClickToEdit(cellId);
+      // Prevents event from bubbling up inside editing cell
+      // Otherwise requires special handling in EditingBodyCell
+      if (editingCellId !== cellId) {
+        onCellClickToEdit(cellId);
+      }
     },
-    [onCellClickToEdit],
+    [onCellClickToEdit, editingCellId],
   );
 
   return <DataGrid {...tableProps} onBodyCellClick={handleCellClick} />;
diff --git a/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.config.ts b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.config.ts
new file mode 100644
index 000000000000..16cdd2268f98
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.config.ts
@@ -0,0 +1,15 @@
+import { Combobox } from "@mantine/core";
+
+import ComboboxStyles from "./Combobox.module.css";
+
+export const comboboxOverrides = {
+  Combobox: Combobox.extend({
+    defaultProps: {
+      size: "md",
+    },
+    classNames: {
+      empty: ComboboxStyles.empty,
+      option: ComboboxStyles.option,
+    },
+  }),
+};
diff --git a/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.mdx b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.mdx
new file mode 100644
index 000000000000..ba9857ea8732
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.mdx
@@ -0,0 +1,35 @@
+import { Canvas, Story, Meta } from "@storybook/blocks";
+import * as ComboboxStories from "./Combobox.stories";
+
+<Meta of={ComboboxStories} />
+
+# Combobox
+
+Our themed wrapper around [Mantine Combobox](https://mantine.dev/core/combobox/).
+
+## Docs
+
+- [Mantine Select Docs](https://mantine.dev/core/combobox/)
+## Examples
+
+<Canvas story={{ height: "200px" }}>
+  <Story of={ComboboxStories.Default} />
+</Canvas>
+
+#### Min Popover Width
+
+<Canvas story={{ height: "200px" }}>
+  <Story of={ComboboxStories.MinWidth} />
+</Canvas>
+
+#### Async Data Fetch
+
+<Canvas story={{ height: "200px" }}>
+  <Story of={ComboboxStories.AsyncDataFetch} />
+</Canvas>
+
+#### Searchable (Async)
+
+<Canvas story={{ height: "200px" }}>
+  <Story of={ComboboxStories.AsyncSearchable} />
+</Canvas>
diff --git a/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.module.css b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.module.css
new file mode 100644
index 000000000000..fb27d615ef84
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.module.css
@@ -0,0 +1,9 @@
+.empty {
+  color: var(--mb-color-text-light);
+}
+
+.option {
+  &:not([data-combobox-selected]):hover {
+    background-color: var(--mb-color-bg-medium);
+  }
+}
diff --git a/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.stories.tsx b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.stories.tsx
new file mode 100644
index 000000000000..d3b6ad677c62
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Combobox/Combobox.stories.tsx
@@ -0,0 +1,163 @@
+import { useEffect, useState } from "react";
+
+import {
+  Button,
+  Center,
+  Combobox,
+  Stack,
+  Text,
+  useCombobox,
+} from "metabase/ui";
+
+const groceries = [
+  "🍎 Apples",
+  "🍌 Bananas",
+  "🥦 Broccoli",
+  "🥕 Carrots",
+  "🍫 Chocolate",
+  "🍇 Grapes",
+];
+
+const args = {
+  minWidth: 0,
+  data: groceries,
+  size: "md",
+  asyncDataFetch: false,
+  searchable: false,
+};
+
+const argTypes = {
+  data: {
+    control: { type: "json" },
+  },
+  size: {
+    options: ["sm", "md", "lg"],
+    control: { type: "inline-radio" },
+  },
+  asyncDataFetch: {
+    control: { type: "boolean" },
+  },
+  minWidth: {
+    control: { type: "number" },
+  },
+  searchable: {
+    control: { type: "boolean" },
+  },
+};
+
+type StorybookProps = typeof args;
+
+function ComboboxTemplate(props: StorybookProps) {
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => combobox.resetSelectedOption(),
+  });
+
+  const [value, setValue] = useState<string | undefined>();
+  const [search, setSearch] = useState("");
+  const [data] = useState<string[]>(props.data);
+  const [loading, setLoading] = useState(props.asyncDataFetch);
+
+  // Emulate loading
+  useEffect(() => {
+    if (props.asyncDataFetch) {
+      setLoading(true);
+      setTimeout(() => {
+        setLoading(false);
+      }, 1000);
+    }
+  }, [props.asyncDataFetch]);
+
+  const options = data
+    .filter(item => item.toLowerCase().includes(search.toLowerCase().trim()))
+    .map(item => (
+      <Combobox.Option value={item} key={item} selected={item === value}>
+        {item}
+      </Combobox.Option>
+    ));
+
+  return (
+    <Combobox
+      size={props.size}
+      store={combobox}
+      withinPortal={false}
+      onOptionSubmit={val => {
+        setValue(val);
+        combobox.closeDropdown();
+      }}
+    >
+      <Center>
+        <Stack gap="xs">
+          <Text>
+            Selected: <strong>{value ?? "Nothing"}</strong>
+          </Text>
+
+          <Combobox.Target>
+            <Button onClick={() => combobox.toggleDropdown()}>
+              Toggle Combobox
+            </Button>
+          </Combobox.Target>
+        </Stack>
+      </Center>
+
+      <Combobox.Dropdown miw={props.minWidth || undefined} p="2px">
+        {props.searchable && (
+          <Combobox.Search
+            value={search}
+            placeholder="Search..."
+            onChange={event => setSearch(event.target.value)}
+          />
+        )}
+        <Combobox.Options>
+          {loading ? (
+            <Combobox.Empty>Loading...</Combobox.Empty>
+          ) : options.length > 0 ? (
+            options
+          ) : (
+            <Combobox.Empty>No results found</Combobox.Empty>
+          )}
+        </Combobox.Options>
+      </Combobox.Dropdown>
+    </Combobox>
+  );
+}
+
+export default {
+  title: "Components/Inputs/Combobox",
+  component: Combobox,
+  args,
+  argTypes,
+};
+
+export const Default = {
+  render: ComboboxTemplate,
+  args: {
+    size: "md",
+  },
+};
+
+export const MinWidth = {
+  render: ComboboxTemplate,
+  args: {
+    minWidth: 300,
+  },
+};
+
+export const AsyncDataFetch = {
+  render: ComboboxTemplate,
+  args: {
+    size: "md",
+    asyncDataFetch: true,
+  },
+};
+
+export const AsyncSearchable = {
+  render: ComboboxTemplate,
+  args: {
+    size: "md",
+    minWidth: 300,
+    asyncDataFetch: true,
+    searchable: true,
+    creatable: true,
+  },
+};
diff --git a/frontend/src/metabase/ui/components/inputs/Combobox/index.ts b/frontend/src/metabase/ui/components/inputs/Combobox/index.ts
new file mode 100644
index 000000000000..e88cfec19008
--- /dev/null
+++ b/frontend/src/metabase/ui/components/inputs/Combobox/index.ts
@@ -0,0 +1,2 @@
+export { Combobox, useCombobox } from "@mantine/core";
+export { comboboxOverrides } from "./Combobox.config";
diff --git a/frontend/src/metabase/ui/components/inputs/index.ts b/frontend/src/metabase/ui/components/inputs/index.ts
index 72c5439085da..2400cfd72569 100644
--- a/frontend/src/metabase/ui/components/inputs/index.ts
+++ b/frontend/src/metabase/ui/components/inputs/index.ts
@@ -20,3 +20,4 @@ export * from "./TextInput";
 export * from "./TextInputBlurChange";
 export * from "./Textarea";
 export * from "./TimeInput";
+export * from "./Combobox";
diff --git a/frontend/src/metabase/ui/theme.ts b/frontend/src/metabase/ui/theme.ts
index 88792e2aaeb5..59035a8e8310 100644
--- a/frontend/src/metabase/ui/theme.ts
+++ b/frontend/src/metabase/ui/theme.ts
@@ -17,6 +17,7 @@ import {
   checkboxOverrides,
   chipOverrides,
   codeOverrides,
+  comboboxOverrides,
   dateInputOverrides,
   datePickerOverrides,
   dividerOverrides,
@@ -132,6 +133,7 @@ export const getThemeOverrides = (): MantineThemeOverride => ({
     ...checkboxOverrides,
     ...chipOverrides,
     ...codeOverrides,
+    ...comboboxOverrides,
     ...dateInputOverrides,
     ...datePickerOverrides,
     ...dividerOverrides,


Reviews:
user: login: deniskaber
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T20:00:19Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:30:25Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:30:38Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:30:57Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:31:12Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:31:20Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:31:25Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:31:35Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:31:42Z
user: login: spalt08
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:33:00Z
user: login: deniskaber
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:05:59Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:26:54Z
user: login: iethree
body: combobox addition looks good to me. and nice storybook stories! should definitely send it straight to master though
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:27:50Z

Review comments:
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: `field_ref` was a hack for PoC. I think it is possible to use just `datasetColumn.id` here
created_at: 2025-03-10T19:36:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: Should we memoize this function as well to avoid combobox recreation on rerender?
created_at: 2025-03-10T19:37:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                className={S.comboboxOption}
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty p="0.75rem" c="var(--mb-color-text-light)">
+              Loading values...
+            </Combobox.Empty>
+          ) : (
+            <Combobox.Option className={S.comboboxOption} value={search}>
+              {`+ Create: "${search}"`}
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: We have a design for this - https://www.figma.com/design/apDrVPGAi63oa9FihfudQp/Internal-Tools-Short-Term-Vision-Mocks?node-id=91-53145&m=dev

And don't forget to use 
```
t``
```
created_at: 2025-03-10T19:43:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                className={S.comboboxOption}
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty p="0.75rem" c="var(--mb-color-text-light)">
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: `text-light` is a default color, we can use it directly
```suggestion
            <Combobox.Empty p="0.75rem" c="text-light">
```
created_at: 2025-03-10T19:44:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                className={S.comboboxOption}
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty p="0.75rem" c="var(--mb-color-text-light)">
+              Loading values...
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: ```suggestion
              {t`Loading values...`}
```
created_at: 2025-03-10T19:44:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: ```suggestion
            placeholder={t`Search the list`}
```
created_at: 2025-03-10T19:44:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,10 @@
-export { rem, useMantineTheme } from "@mantine/core";
+export {
+  rem,
+  useMantineTheme,
+  Combobox,
path: frontend/src/metabase/ui/index.ts
user: login: deniskaber
body: Please check how other components from mantine are added in `frontend/src/metabase/ui/components`.

You have to add a story and an override file. Check recent PRs that add components there. E.g. https://github.com/metabase/metabase/commit/156a329589bd754f061b52db8139b31144a3c7df#diff-5bd78eb03465f4df1a2ac792bb20b8378aa3b766718ab6f5bf387ac341dba449
created_at: 2025-03-10T19:56:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: Why not a regular input here or just a button ? I think it is better to avoid adding a separate component from mantine just for this.
created_at: 2025-03-10T19:59:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: We should memoize this, I think
created_at: 2025-03-10T19:59:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Fixed in 23dbb83
created_at: 2025-03-11T12:30:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Decided to pass `onCancel` directly, fixed in 23dbb83
created_at: 2025-03-11T12:30:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                className={S.comboboxOption}
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty p="0.75rem" c="var(--mb-color-text-light)">
+              Loading values...
+            </Combobox.Empty>
+          ) : (
+            <Combobox.Option className={S.comboboxOption} value={search}>
+              {`+ Create: "${search}"`}
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Fixed in 23dbb83
created_at: 2025-03-11T12:30:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                className={S.comboboxOption}
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty p="0.75rem" c="var(--mb-color-text-light)">
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Fixed with component overrides in 23dbb83
created_at: 2025-03-11T12:31:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                className={S.comboboxOption}
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty p="0.75rem" c="var(--mb-color-text-light)">
+              Loading values...
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Fixed in 23dbb83
created_at: 2025-03-11T12:31:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
+          component="button"
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          className={S.input}
+          size="xs"
+        >
+          {initialValue}
+        </InputBase>
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder="Search the list"
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Fixed in 23dbb83
created_at: 2025-03-11T12:31:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,10 @@
-export { rem, useMantineTheme } from "@mantine/core";
+export {
+  rem,
+  useMantineTheme,
+  Combobox,
path: frontend/src/metabase/ui/index.ts
user: login: spalt08
body: Added a component: 23dbb83
created_at: 2025-03-11T12:31:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const fieldId = datasetColumn.field_ref?.[1] as FieldId | null;
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    fieldId ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: () => {
+      onCancel();
+    },
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <InputBase
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Changed to input 23dbb83
created_at: 2025-03-11T12:31:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,7 +131,93 @@ export const EditingBodyCell = ({
       autoFocus
       onChange={handleChange}
       onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onBlur={() => onSubmit(value)}
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: spalt08
body: Removed controlled input and added basic event handlers without memoization, so the component should never re-render in theory, see the thread:
https://metaboat.slack.com/archives/C505ZNNH4/p1740696146782029
created_at: 2025-03-11T12:33:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,4 @@
+// should be in sync with node_modules/@tanstack/table-core/src/core/cell.ts logic
+export const getGridCellId = (rowIndex: number, columnName: string): string => {
path: frontend/src/metabase/data-grid/utils/cell.ts
user: login: deniskaber
body: This file can be removed - not used for now
created_at: 2025-03-11T15:01:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+import { Combobox } from "@mantine/core";
path: frontend/src/metabase/ui/components/inputs/Combobox/Combobox.config.ts
user: login: deniskaber
body: I think it would be better to extract adding Mantine Combobox component from this PR into a separate small one targeting master branch and merge it separately as our feature branch would not get merged for quite a while. and in a meantime, Combobox can be used by other devs.
created_at: 2025-03-11T15:03:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,87 +1,193 @@
 import type { CellContext } from "@tanstack/react-table";
-import {
-  type ChangeEvent,
-  type KeyboardEvent,
-  useCallback,
-  useState,
-} from "react";
+import { useCallback, useMemo, useState } from "react";
+import { t } from "ttag";
 
-import { Input } from "metabase/ui";
-import type { RowValue, RowValues } from "metabase-types/api";
+import { skipToken, useGetFieldValuesQuery } from "metabase/api";
+import { getFieldOptions } from "metabase/querying/filters/components/FilterValuePicker/utils";
+import {
+  Box,
+  Combobox,
+  Icon,
+  Input,
+  TextInput,
+  useCombobox,
+} from "metabase/ui";
+import type { DatasetColumn, RowValue, RowValues } from "metabase-types/api";
 
-import S from "./TableDataView.module.css";
+import S from "./EditingBodyCell.module.css";
 import type { UpdatedRowCellsHandlerParams } from "./types";
 
 interface EditingBodyCellProps<TRow, TValue> {
+  column: DatasetColumn;
   cellContext: CellContext<TRow, TValue>;
   onCellValueUpdate: (params: UpdatedRowCellsHandlerParams) => void;
   onCellEditCancel: () => void;
 }
 
-export const EditingBodyCell = ({
-  cellContext,
-  onCellValueUpdate,
-  onCellEditCancel,
-}: EditingBodyCellProps<RowValues, RowValue>) => {
+export const EditingBodyCellConditional = (
+  props: EditingBodyCellProps<RowValues, RowValue>,
+) => {
   const {
-    getValue,
-    row: { index: rowIndex },
-    column: { id: columnName },
-  } = cellContext;
-  const initialValue = getValue<RowValue>();
-  const [value, setValue] = useState<RowValue>(initialValue);
-
-  const doCellValueUpdate = useCallback(() => {
-    if (value !== initialValue) {
-      onCellValueUpdate({
-        data: {
-          [columnName]: value,
-        },
-        rowIndex,
-      });
-    }
-
-    onCellEditCancel();
-  }, [
-    columnName,
-    initialValue,
     onCellEditCancel,
     onCellValueUpdate,
-    rowIndex,
-    value,
-  ]);
+    column,
+    cellContext: {
+      getValue,
+      column: { id: columnName },
+      row: { index: rowIndex },
+    },
+  } = props;
 
-  const handleChange = useCallback(
-    (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value),
-    [],
-  );
+  const initialValue = getValue<RowValue>();
 
-  const handleKeyUp = useCallback(
-    (e: KeyboardEvent<HTMLInputElement>) => {
-      if (e.key === "Enter") {
-        doCellValueUpdate();
-      }
-      if (e.key === "Escape") {
-        onCellEditCancel();
+  const doCellValueUpdate = useCallback(
+    (value: RowValue) => {
+      if (value !== initialValue) {
+        onCellValueUpdate({
+          data: { [columnName]: value },
+          rowIndex,
+        });
       }
+
+      // Hide the editing cell after submitting the value
+      onCellEditCancel();
     },
-    [doCellValueUpdate, onCellEditCancel],
+    [columnName, onCellEditCancel, onCellValueUpdate, rowIndex, initialValue],
   );
 
-  const handleFieldBlur = useCallback(() => {
-    doCellValueUpdate();
-  }, [doCellValueUpdate]);
+  switch (column?.semantic_type) {
+    case "type/State":
+    case "type/Country":
+    case "type/Category":
+      return (
+        <EditingBodyCellSelect
+          initialValue={initialValue}
+          datasetColumn={column}
+          onSubmit={doCellValueUpdate}
+          onCancel={onCellEditCancel}
+        />
+      );
+
+    default:
+      return (
+        <EditingBodyCellText
+          initialValue={initialValue}
+          datasetColumn={column}
+          onSubmit={doCellValueUpdate}
+          onCancel={onCellEditCancel}
+        />
+      );
+  }
+};
+
+interface EditingBodyPrimitiveProps {
+  datasetColumn: DatasetColumn;
+  initialValue: RowValue;
+  onSubmit: (value: RowValue) => unknown;
+  onCancel: () => unknown;
+}
 
+export const EditingBodyCellText = ({
+  initialValue,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
   return (
     <Input
-      value={value as any} // TODO [Milestone 2]: fixup this type after adding specific inputs based on data type
+      defaultValue={(initialValue ?? "").toString()}
       className={S.input}
       variant="unstyled"
       size="xs"
       autoFocus
-      onChange={handleChange}
-      onKeyUp={handleKeyUp}
-      onBlur={handleFieldBlur}
+      onKeyUp={event => {
+        if (event.key === "Escape") {
+          onCancel();
+        } else if (event.key === "Enter") {
+          onSubmit(event.currentTarget.value);
+        }
+      }}
+      onBlur={event => {
+        onSubmit(event.currentTarget.value);
+      }}
     />
   );
 };
+
+export const EditingBodyCellSelect = ({
+  initialValue,
+  datasetColumn,
+  onSubmit,
+  onCancel,
+}: EditingBodyPrimitiveProps) => {
+  const { data: fieldData, isLoading } = useGetFieldValuesQuery(
+    datasetColumn.id ?? skipToken,
+  );
+
+  const [search, setSearch] = useState("");
+  const combobox = useCombobox({
+    defaultOpened: true,
+    onDropdownClose: onCancel,
+  });
+
+  const options = useMemo(
+    () =>
+      fieldData
+        ? getFieldOptions(fieldData.values).filter(item =>
+            item.label.toLowerCase().includes(search.toLowerCase().trim()),
+          )
+        : [],
+    [fieldData, search],
+  );
+
+  return (
+    <Combobox
+      store={combobox}
+      position="bottom-start"
+      onOptionSubmit={onSubmit}
+    >
+      <Combobox.Target>
+        <Input
+          value={(initialValue ?? "").toString()}
+          variant="unstyled"
+          pointer
+          onClick={() => combobox.toggleDropdown()}
+          onMouseDown={onCancel}
+          className={S.input}
+          size="xs"
+        />
+      </Combobox.Target>
+
+      <Combobox.Dropdown mah="none" miw={250}>
+        <Box p="0.5rem" pb="0 " bg="white" pos="sticky" top={0}>
+          <TextInput
+            value={search}
+            onChange={event => setSearch(event.currentTarget.value)}
+            placeholder={t`Search the list`}
+            leftSection={<Icon name="search" />}
+            autoFocus
+          />
+        </Box>
+
+        <Combobox.Options p="0.5rem">
+          {options.length > 0 ? (
+            options.map(item => (
+              <Combobox.Option
+                selected={initialValue === item.value}
+                value={item.value}
+                key={item.value}
+              >
+                {item.label}
+              </Combobox.Option>
+            ))
+          ) : isLoading ? (
+            <Combobox.Empty>{t`Loading values...`}</Combobox.Empty>
+          ) : (
+            <Combobox.Option value={search}>
+              {t`Add option:`} <strong>{search}</strong>
path: enterprise/frontend/src/metabase-enterprise/data_editing/tables/EditingBodyCell.tsx
user: login: deniskaber
body: nit: I think overall we want to use styles instead of `<strong>`, but I don't think it is a big deal in this case.
created_at: 2025-03-11T15:05:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+import { Combobox } from "@mantine/core";
path: frontend/src/metabase/ui/components/inputs/Combobox/Combobox.config.ts
user: login: iethree
body: ☝️  💯 

created_at: 2025-03-11T16:26:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-03-10T18:38:09Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/23591_tests-281d9f5a-purple)](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/0-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/6-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=QUARANTINED)


| Flaky Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>issue 29951 should allow to run the model query after changing custom columns (metabase\#29951)</code> | The \`cy.trigger()\` command failed because the page updated and the element was removed from the DOM, causing Cypress to lose track of the element. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=FAILURE&runId=36f28ca6-2932-55bf-9f35-cf07d61fb9d7) |
| <code>issue 48829 should not show the unsaved changes warning when switching back to chill mode from the notebook editor after adding a filter from heade...</code> | \`cy.within()\` can only be called on a single element, but the subject contains 2 elements. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=FAILURE&runId=d5d23a2d-76bf-5cf4-84dd-26b0abfa70c9) |
| <code>issue 45063 type/FK -> column remapping (metabase\#45063) should work with native models</code> | \`cy.within()\` can only be called on a single element, but the subject contained 2 elements. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=FAILURE&runId=d3c62b76-91b0-5498-a654-e978650785f8) |
| <code>issue 45063 type/FK -> column remapping (metabase\#45063) should work with models</code> | \`cy.within()\` can only be used on a single element, but the subject contains 2 elements. Use \`.first()\` or \`.each()\` instead. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=FAILURE&runId=b3f4dbe0-3e92-5ed4-a173-f1d2b965be69) |

[... and 2 more](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment&runConclusion=FAILURE)
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/54921?repo=metabase/metabase&commitHash=281d9f5aa4e1acebb68d1393c23d2c87bd53b696&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-11T15:27:24Z
body: 
  ## e2e tests failed on `281d9f5aa4e1acebb68d1393c23d2c87bd53b696-1`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/13791518666?pr=54921)

  File | Test Name
  ---- | ---------
  `reproductions-3.cy.spec.js` | issue 45063 > type/PK -> type/Name remapping (metabase#45063) > should work with models
`reproductions-3.cy.spec.js` | issue 45063 > type/FK -> column remapping (metabase#45063) > should work with questions
`reproductions-3.cy.spec.js` | issue 45063 > type/FK -> column remapping (metabase#45063) > should work with models
`reproductions-3.cy.spec.js` | issue 45063 > type/FK -> column remapping (metabase#45063) > should work with native models
`reproductions-3.cy.spec.js` | issue 48829 > should not show the unsaved changes warning when switching back to chill mode from the notebook editor after adding a filter from headers (metabase#48829)
`reproductions.cy.spec.js` | issue 29951 > should allow to run the model query after changing custom columns (metabase#29951)

author_association: CONTRIBUTOR
user: login: spalt08
created_at: 2025-03-11T16:52:07Z
body: @iethree Thanks. I'll make a separate PR with a copied folder to not block us with feature branch development 
author_association: CONTRIBUTOR