From cef158e825702de661b59dbc2680320ae942e1b2 Mon Sep 17 00:00:00 2001
From: Nethmi Rodrigo <neth23rodrigo@gmail.com>
Date: Fri, 10 Jan 2025 17:13:01 +0530
Subject: [PATCH] Break into select rendering

---
 .../modals/question/form-field-context.tsx    |   9 +-
 .../concept-search.component.tsx              |   0
 .../common/concept-search/concept-search.scss |  67 +++++++
 .../concept-search/concept-search.test.tsx    | 134 +++++++++++++
 .../required-label.component.tsx              |   0
 .../required-label/required-label.scss        |   3 +-
 .../obs/obs-type-question.component.tsx       | 186 ++----------------
 .../inputs/obs/obs-type-question.scss         |  90 +--------
 .../inputs/obs/obs-type-question.test.tsx     | 132 +------------
 .../question/question.component.tsx           |   2 +-
 .../rendering-types/inputs/index.tsx          |   1 +
 .../select/select-answers.component.tsx       | 184 +++++++++++++++++
 .../inputs/select/select-answers.scss         |  25 +++
 .../inputs/select/select-answers.test.tsx     | 125 ++++++++++++
 .../rendering-type.component.tsx              |   5 +-
 15 files changed, 569 insertions(+), 394 deletions(-)
 rename src/components/interactive-builder/modals/question/question-form/{question-types/inputs/obs => common/concept-search}/concept-search.component.tsx (100%)
 create mode 100644 src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.scss
 create mode 100644 src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.test.tsx
 rename src/components/interactive-builder/modals/question/question-form/{ => common}/required-label/required-label.component.tsx (100%)
 rename src/components/interactive-builder/modals/question/question-form/{ => common}/required-label/required-label.scss (55%)
 create mode 100644 src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.component.tsx
 create mode 100644 src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.scss
 create mode 100644 src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.test.tsx

diff --git a/src/components/interactive-builder/modals/question/form-field-context.tsx b/src/components/interactive-builder/modals/question/form-field-context.tsx
index ca4c2acc..7ac8109a 100644
--- a/src/components/interactive-builder/modals/question/form-field-context.tsx
+++ b/src/components/interactive-builder/modals/question/form-field-context.tsx
@@ -1,9 +1,12 @@
 import React, { createContext, useCallback, useContext, useState, type ReactNode } from 'react';
 import type { FormField } from '@openmrs/esm-form-engine-lib';
+import type { Concept } from '@types';
 
 interface FormFieldContextType {
   formField: FormField;
   setFormField: React.Dispatch<React.SetStateAction<FormField>>;
+  concept: Concept;
+  setConcept: React.Dispatch<React.SetStateAction<Concept>>;
 }
 
 const FormFieldContext = createContext<FormFieldContextType | undefined>(undefined);
@@ -12,8 +15,10 @@ export const FormFieldProvider: React.FC<{
   children: ReactNode;
   initialFormField: FormField;
   isObsGrouped?: boolean;
-}> = ({ children, initialFormField, isObsGrouped = false }) => {
+  selectedConcept?: Concept;
+}> = ({ children, initialFormField, isObsGrouped = false, selectedConcept = null }) => {
   const [formField, setFormField] = useState<FormField>(initialFormField);
+  const [concept, setConcept] = useState<Concept | null>(selectedConcept);
 
   const updateObsGroupedQuestion = useCallback(
     (updatedObsGroupFormField: FormField) => {
@@ -33,7 +38,7 @@ export const FormFieldProvider: React.FC<{
 
   return (
     <FormFieldContext.Provider
-      value={{ formField, setFormField: isObsGrouped ? updateObsGroupedQuestion : setFormField }}
+      value={{ formField, setFormField: isObsGrouped ? updateObsGroupedQuestion : setFormField, concept, setConcept }}
     >
       {children}
     </FormFieldContext.Provider>
diff --git a/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/concept-search.component.tsx b/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.component.tsx
similarity index 100%
rename from src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/concept-search.component.tsx
rename to src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.component.tsx
diff --git a/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.scss b/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.scss
new file mode 100644
index 00000000..112704eb
--- /dev/null
+++ b/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.scss
@@ -0,0 +1,67 @@
+@use '@carbon/colors';
+@use '@carbon/layout';
+@use '@carbon/type';
+
+.label {
+  margin-bottom: layout.$spacing-02;
+}
+
+.error {
+  width: 100%;
+  max-width: unset;
+  padding: '0rem';
+  margin-bottom: $spacing-05;
+}
+
+.loader {
+  padding: layout.$spacing-04 $spacing-03;
+}
+
+.concept {
+  padding: $spacing-04;
+  border-bottom: 1px solid colors.$gray-20;
+
+  &:last-of-type {
+    border-bottom: none;
+  }
+}
+
+.conceptList {
+  background: colors.$white-0;
+  max-height: 14rem;
+  overflow-y: auto;
+  border: 1px solid colors.$gray-20;
+  border-top: none;
+}
+
+.conceptList li:hover {
+  background-color: colors.$gray-20;
+}
+
+.emptyResults {
+  @include type.type-style('body-compact-01');
+  color: colors.$gray-70;
+  min-height: layout.$spacing-05;
+}
+
+.oclLauncherBanner {
+  padding: layout.$spacing-03 layout.$spacing-05;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: colors.$gray-50;
+
+  p {
+    color: colors.$white-0;
+  }
+}
+
+.oclLink {
+  color: colors.$blue-10;
+  display: flex;
+  align-items: center;
+
+  svg {
+    margin-left: 0.25rem;
+  }
+}
diff --git a/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.test.tsx b/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.test.tsx
new file mode 100644
index 00000000..2bbcd026
--- /dev/null
+++ b/src/components/interactive-builder/modals/question/question-form/common/concept-search/concept-search.test.tsx
@@ -0,0 +1,134 @@
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from '@testing-library/react';
+import { useConceptId } from '@hooks/useConceptId';
+import { useConceptLookup } from '@hooks/useConceptLookup';
+import ConceptSearch from './concept-search.component';
+import type { Concept } from '@types';
+
+const concepts: Array<Concept> = [
+  {
+    uuid: '123',
+    display: 'Concept 1',
+    datatype: { uuid: '456', name: 'Coded' },
+    mappings: [{ display: 'CIEL:1606', conceptMapType: { display: 'SAME-AS' } }],
+    answers: [
+      { uuid: '1', display: 'Answer 1' },
+      { uuid: '2', display: 'Answer 2' },
+    ],
+  },
+  {
+    uuid: '456',
+    display: 'Concept 2',
+    datatype: { uuid: '456', name: 'Date' },
+    mappings: [{ display: 'CIEL:1656', conceptMapType: { display: 'SAME-AS' } }],
+  },
+];
+const mockUseConceptLookup = jest.mocked(useConceptLookup);
+jest.mock('@hooks/useConceptLookup', () => ({
+  ...jest.requireActual('@hooks/useConceptLookup'),
+  useConceptLookup: jest.fn(),
+}));
+const mockUseConceptId = jest.mocked(useConceptId);
+jest.mock('@hooks/useConceptId', () => ({
+  ...jest.requireActual('@hooks/useConceptId'),
+  useConceptId: jest.fn(),
+}));
+const onSelectConcept = jest.fn();
+
+describe('Concept search component', () => {
+  beforeEach(() => {
+    mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: false });
+    mockUseConceptId.mockReturnValue({
+      concept: null,
+      conceptName: null,
+      conceptNameLookupError: null,
+      isLoadingConcept: false,
+    });
+  });
+  it('renders', () => {
+    renderComponent();
+    expect(screen.getByRole('searchbox', { name: /search for a backing concept/i })).toBeInTheDocument();
+  });
+
+  it('allows user to search and select a concept', async () => {
+    mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: false });
+    mockUseConceptId.mockReturnValue({
+      concept: null,
+      conceptName: null,
+      conceptNameLookupError: null,
+      isLoadingConcept: false,
+    });
+    const user = userEvent.setup();
+    renderComponent();
+
+    const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
+    await user.click(searchInput);
+    await user.type(searchInput, 'Concept 1');
+
+    const conceptMenuItem = await screen.findByRole('menuitem', {
+      name: /concept 1/i,
+    });
+    expect(conceptMenuItem).toBeInTheDocument();
+    expect(searchInput).toHaveDisplayValue(/concept 1/i);
+  });
+
+  it('shows loading spinner when concept is loading', async () => {
+    mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: true });
+    mockUseConceptId.mockReturnValue({
+      concept: null,
+      conceptName: null,
+      conceptNameLookupError: null,
+      isLoadingConcept: false,
+    });
+    const user = userEvent.setup();
+    renderComponent();
+
+    const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
+    await user.click(searchInput);
+    await user.type(searchInput, 'Does not exist');
+    expect(screen.getByText(/searching\.\.\./i)).toBeInTheDocument();
+  });
+
+  it('displays an error message if the searched concept cannot be found', async () => {
+    mockUseConceptLookup.mockReturnValue({ concepts: [], conceptLookupError: Error(), isLoadingConcepts: false });
+    mockUseConceptId.mockReturnValue({
+      concept: null,
+      conceptName: null,
+      conceptNameLookupError: null,
+      isLoadingConcept: false,
+    });
+    renderComponent();
+
+    expect(screen.getByText(/error fetching concepts/i)).toBeInTheDocument();
+    expect(screen.getByText(/please try again\./i)).toBeInTheDocument();
+  });
+
+  it('displays an error with a link to ocl when concept query gives empty results', async () => {
+    mockUseConceptLookup.mockReturnValue({ concepts: [], conceptLookupError: null, isLoadingConcepts: false });
+    mockUseConceptId.mockReturnValue({
+      concept: null,
+      conceptName: null,
+      conceptNameLookupError: null,
+      isLoadingConcept: false,
+    });
+    const user = userEvent.setup();
+    renderComponent();
+
+    const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
+    await user.click(searchInput);
+    await user.type(searchInput, 'Does not exist');
+
+    expect(screen.getByText(/no concepts were found that match/i)).toBeInTheDocument();
+    expect(screen.getByText(/can't find a concept\?/i)).toBeInTheDocument();
+    expect(
+      screen.getByRole('link', {
+        name: /search in ocl/i,
+      }),
+    ).toBeInTheDocument();
+  });
+});
+
+function renderComponent() {
+  render(<ConceptSearch onSelectConcept={onSelectConcept} />);
+}
diff --git a/src/components/interactive-builder/modals/question/question-form/required-label/required-label.component.tsx b/src/components/interactive-builder/modals/question/question-form/common/required-label/required-label.component.tsx
similarity index 100%
rename from src/components/interactive-builder/modals/question/question-form/required-label/required-label.component.tsx
rename to src/components/interactive-builder/modals/question/question-form/common/required-label/required-label.component.tsx
diff --git a/src/components/interactive-builder/modals/question/question-form/required-label/required-label.scss b/src/components/interactive-builder/modals/question/question-form/common/required-label/required-label.scss
similarity index 55%
rename from src/components/interactive-builder/modals/question/question-form/required-label/required-label.scss
rename to src/components/interactive-builder/modals/question/question-form/common/required-label/required-label.scss
index 19e5da34..a73d5ee7 100644
--- a/src/components/interactive-builder/modals/question/question-form/required-label/required-label.scss
+++ b/src/components/interactive-builder/modals/question/question-form/common/required-label/required-label.scss
@@ -1,6 +1,7 @@
 @use '@carbon/colors';
+@use '@carbon/layout';
 
 .required {
   color: colors.$red-60;
-  margin-left: 0.125rem;
+  margin-left: $spacing-02;
 }
diff --git a/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.component.tsx b/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.component.tsx
index b4c365d9..340f1b70 100644
--- a/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.component.tsx
+++ b/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.component.tsx
@@ -1,7 +1,7 @@
 import React, { useCallback, useMemo, useRef, useState } from 'react';
 import { FormLabel, InlineNotification, MultiSelect, FormGroup, Tag, Stack } from '@carbon/react';
 import { useTranslation } from 'react-i18next';
-import ConceptSearch from './concept-search.component';
+import ConceptSearch from '../../../common/concept-search/concept-search.component';
 import { useFormField } from '../../../../form-field-context';
 import type { Concept, ConceptMapping, DatePickerType } from '@types';
 import styles from './obs-type-question.scss';
@@ -13,24 +13,8 @@ interface AnswerItem {
 
 const ObsTypeQuestion: React.FC = () => {
   const { t } = useTranslation();
-  const { formField, setFormField } = useFormField();
-  const [selectedConcept, setSelectedConcept] = useState<Concept>(null);
-  const [selectedAnswers, setSelectedAnswers] = useState<Array<AnswerItem>>(
-    formField.questionOptions?.answers
-      ? formField.questionOptions.answers.map((answer) => ({
-          id: answer.concept,
-          text: answer.label,
-        }))
-      : [],
-  );
-  const [addedAnswers, setAddedAnswers] = useState<AnswerItem[]>([]);
+  const { formField, setFormField, concept, setConcept } = useFormField();
   const [conceptMappings, setConceptMappings] = useState<Array<ConceptMapping>>([]);
-  const initiallySelectedAnswers = useRef(
-    formField?.questionOptions?.answers?.map((answer) => ({
-      id: answer.concept,
-      text: answer.label,
-    })),
-  );
 
   const getDatePickerType = useCallback((concept: Concept): DatePickerType | null => {
     const conceptDataType = concept.datatype.name;
@@ -47,20 +31,20 @@ const ObsTypeQuestion: React.FC = () => {
   }, []);
 
   const handleConceptSelect = useCallback(
-    (concept: Concept) => {
-      setSelectedConcept(concept);
-      if (concept) {
-        const datePickerType = getDatePickerType(concept);
+    (selectedConcept: Concept) => {
+      setConcept(selectedConcept);
+      if (selectedConcept) {
+        const datePickerType = getDatePickerType(selectedConcept);
         setFormField((prevField) => ({
           ...prevField,
           questionOptions: {
             ...prevField.questionOptions,
-            concept: concept.uuid,
+            concept: selectedConcept.uuid,
           },
           ...(datePickerType && { datePickerFormat: datePickerType }),
         }));
         setConceptMappings(
-          concept?.mappings?.map((conceptMapping) => {
+          selectedConcept?.mappings?.map((conceptMapping) => {
             const data = conceptMapping.display.split(': ');
             return {
               relationship: conceptMapping.conceptMapType.display,
@@ -71,11 +55,11 @@ const ObsTypeQuestion: React.FC = () => {
         );
       }
     },
-    [getDatePickerType, setFormField],
+    [getDatePickerType, setFormField, setConcept],
   );
 
   const clearSelectedConcept = useCallback(() => {
-    setSelectedConcept(null);
+    setConcept(null);
     setConceptMappings([]);
 
     setFormField((prevFormField) => {
@@ -89,104 +73,7 @@ const ObsTypeQuestion: React.FC = () => {
       }
       return updatedFormField;
     });
-  }, [setFormField]);
-
-  const selectAnswers = useCallback(
-    ({ selectedItems }: { selectedItems: Array<AnswerItem> }) => {
-      const mappedAnswers = selectedItems.map((answer) => ({
-        concept: answer.id,
-        label: answer.text,
-      }));
-
-      setSelectedAnswers(selectedItems);
-
-      setFormField((prevField) => {
-        const currentAnswers = prevField.questionOptions?.answers || [];
-        if (JSON.stringify(currentAnswers) === JSON.stringify(mappedAnswers)) {
-          return prevField;
-        }
-        return {
-          ...prevField,
-          questionOptions: {
-            ...prevField.questionOptions,
-            answers: mappedAnswers,
-          },
-        };
-      });
-    },
-    [setFormField],
-  );
-
-  const handleDeleteAdditionalAnswer = useCallback(
-    (id: string) => {
-      setAddedAnswers((prevAnswers) => prevAnswers.filter((answer) => answer.id !== id));
-      setFormField((prevFormField) => {
-        const selectedAnswers = prevFormField.questionOptions?.answers ?? [];
-        return {
-          ...prevFormField,
-          questionOptions: {
-            ...prevFormField.questionOptions,
-            answers: selectedAnswers.filter((answer) => answer.concept !== id),
-          },
-        };
-      });
-    },
-    [setFormField],
-  );
-
-  const handleSelectAdditionalAnswer = useCallback(
-    (concept: Concept) => {
-      const newAnswer = { id: concept.uuid, text: concept.display };
-      const answerExistsInSelected = selectedAnswers.some((answer) => answer.id === newAnswer.id);
-      const answerExistsInAdded = addedAnswers.some((answer) => answer.id === newAnswer.id);
-      if (!answerExistsInSelected && !answerExistsInAdded) {
-        setAddedAnswers((prevAnswers) => [...prevAnswers, newAnswer]);
-        setFormField((prevFormField) => {
-          const existingAnswers = prevFormField.questionOptions?.answers ?? [];
-          existingAnswers.push({ concept: concept.uuid, label: concept.display });
-          return {
-            ...prevFormField,
-            questionOptions: {
-              ...prevFormField.questionOptions,
-              answers: existingAnswers,
-            },
-          };
-        });
-      }
-    },
-    [selectedAnswers, addedAnswers, setFormField],
-  );
-
-  const answerItems = useMemo(() => {
-    // Convert answers from the concept to items format
-    const conceptAnswerItems =
-      selectedConcept?.answers?.map((answer) => ({
-        id: answer.uuid,
-        text: answer.display,
-      })) ?? [];
-
-    const formFieldAnswers = formField.questionOptions?.answers ?? [];
-
-    // If no answers from concept but we have form field answers, use those
-    if (conceptAnswerItems.length === 0 && formFieldAnswers.length > 0) {
-      return formFieldAnswers.map((answer) => ({
-        id: answer.concept,
-        text: answer.label,
-      }));
-    }
-
-    // Merge concept answers with any additional form field answers
-    const additionalAnswers = formFieldAnswers
-      .filter((answer) => !conceptAnswerItems.some((item) => item.id === answer.concept))
-      .map((answer) => ({
-        id: answer.concept,
-        text: answer.label,
-      }));
-
-    return [...conceptAnswerItems, ...additionalAnswers];
-  }, [selectedConcept?.answers, formField.questionOptions?.answers]);
-
-  const convertAnswerItemsToString = useCallback((item: AnswerItem) => item.text, []);
+  }, [setFormField, setConcept]);
 
   return (
     <Stack gap={5}>
@@ -196,7 +83,7 @@ const ObsTypeQuestion: React.FC = () => {
         onSelectConcept={handleConceptSelect}
       />
 
-      {selectedConcept?.allowDecimal === false && (
+      {concept?.allowDecimal === false && (
         <InlineNotification
           kind="info"
           lowContrast
@@ -227,55 +114,6 @@ const ObsTypeQuestion: React.FC = () => {
           </table>
         </FormGroup>
       ) : null}
-
-      {answerItems.length > 0 && (
-        <MultiSelect
-          className={styles.multiSelect}
-          direction="top"
-          id="selectAnswers"
-          items={answerItems}
-          itemToString={convertAnswerItemsToString}
-          onChange={selectAnswers}
-          size="md"
-          selectedItems={selectedAnswers}
-          initialSelectedItems={initiallySelectedAnswers}
-          titleText={t('selectAnswersToDisplay', 'Select answers to display')}
-        />
-      )}
-
-      {selectedAnswers.length > 0 && (
-        <div>
-          {selectedAnswers.map((answer) => (
-            <Tag className={styles.tag} key={answer.id} type={'blue'}>
-              {answer.text}
-            </Tag>
-          ))}
-        </div>
-      )}
-
-      {selectedConcept && selectedConcept.datatype?.name === 'Coded' && (
-        <>
-          <ConceptSearch
-            label={t('searchForAnswerConcept', 'Search for a concept to add as an answer')}
-            onSelectConcept={handleSelectAdditionalAnswer}
-          />
-          {addedAnswers.length > 0 ? (
-            <div>
-              {addedAnswers.map((answer) => (
-                <Tag className={styles.tag} key={answer.id} type={'blue'}>
-                  {answer.text}
-                  <button
-                    className={styles.conceptAnswerButton}
-                    onClick={() => handleDeleteAdditionalAnswer(answer.id)}
-                  >
-                    X
-                  </button>
-                </Tag>
-              ))}
-            </div>
-          ) : null}{' '}
-        </>
-      )}
     </Stack>
   );
 };
diff --git a/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.scss b/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.scss
index bfacc9b9..fd73ec10 100644
--- a/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.scss
+++ b/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.scss
@@ -1,69 +1,8 @@
 @use '@carbon/colors';
 @use '@carbon/layout';
-@use '@carbon/type';
 
 .label {
-  margin-bottom: 0.5rem;
-}
-
-.error {
-  width: 100%;
-  max-width: unset;
-  padding: '0rem';
-  margin-bottom: 1rem;
-}
-
-.loader {
-  padding: 0.75rem 0.5rem;
-}
-
-.conceptList {
-  background: colors.$white-0;
-  max-height: 14rem;
-  overflow-y: auto;
-  border: 1px solid colors.$gray-20;
-  border-top: none;
-}
-
-.conceptList li:hover {
-  background-color: colors.$gray-20;
-}
-
-.concept {
-  padding: 0.75rem;
-  border-bottom: 1px solid colors.$gray-20;
-
-  &:last-of-type {
-    border-bottom: none;
-  }
-}
-
-.emptyResults {
-  @include type.type-style('body-compact-01');
-  color: colors.$gray-70;
-  min-height: 1rem;
-}
-
-.oclLauncherBanner {
-  padding: 0.5rem 1rem;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  background-color: colors.$gray-50;
-
-  p {
-    color: colors.$white-0;
-  }
-}
-
-.oclLink {
-  color: colors.$blue-10;
-  display: flex;
-  align-items: center;
-
-  svg {
-    margin-left: 0.25rem;
-  }
+  margin-bottom: layout.$spacing-03;
 }
 
 .table {
@@ -76,9 +15,9 @@
 
   td,
   th {
-    padding: 0.25rem;
+    padding: layout.$spacing-02;
     border: 1px solid colors.$gray-20;
-    max-width: 10rem;
+    max-width: layout.$spacing-13;
     text-align: left;
   }
 }
@@ -94,26 +33,3 @@
     background-color: colors.$gray-10;
   }
 }
-
-.multiSelect {
-  :global(.cds--list-box__field--wrapper) {
-    align-items: center;
-    display: flex;
-    height: 2.5rem;
-    justify-content: space-between;
-    margin-left: 0.5rem;
-    width: 100%;
-  }
-}
-
-.tag {
-  margin-right: 0.5rem;
-}
-
-.conceptAnswerButton {
-  margin-left: 5px;
-  color: black;
-  background: none;
-  border: none;
-  padding: 0;
-}
diff --git a/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.test.tsx b/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.test.tsx
index efef32e5..ee4d0ba4 100644
--- a/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.test.tsx
+++ b/src/components/interactive-builder/modals/question/question-form/question-types/inputs/obs/obs-type-question.test.tsx
@@ -9,6 +9,7 @@ import type { FormField } from '@openmrs/esm-form-engine-lib';
 import type { Concept } from '@types';
 
 const mockSetFormField = jest.fn();
+const setConcept = jest.fn();
 const formField: FormField = {
   id: '1',
   type: 'obs',
@@ -19,7 +20,7 @@ const formField: FormField = {
 
 jest.mock('../../../../form-field-context', () => ({
   ...jest.requireActual('../../../../form-field-context'),
-  useFormField: () => ({ formField, setFormField: mockSetFormField }),
+  useFormField: () => ({ formField, setFormField: mockSetFormField, setConcept }),
 }));
 
 const concepts: Array<Concept> = [
@@ -66,7 +67,7 @@ describe('ObsTypeQuestion', () => {
     expect(screen.getByRole('searchbox', { name: /search for a backing concept/i })).toBeInTheDocument();
   });
 
-  it('renders the concept details after searching for a concept and displays the concept mappings and answers', async () => {
+  it('renders the concept details after searching for a concept and displays the concept mappings', async () => {
     mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: false });
     mockUseConceptId.mockReturnValue({
       concept: null,
@@ -98,70 +99,6 @@ describe('ObsTypeQuestion', () => {
         name: /same\-as/i,
       }),
     ).toBeInTheDocument();
-
-    const answersMenu = screen.getByRole('combobox', {
-      name: /select answers to display/i,
-    });
-    expect(answersMenu).toBeInTheDocument();
-
-    await user.click(answersMenu);
-    expect(screen.getByText(/answer 1/i)).toBeInTheDocument();
-    expect(screen.getByText(/answer 2/i)).toBeInTheDocument();
-  });
-
-  it('displays an error with a link to ocl when concept query gives empty results', async () => {
-    mockUseConceptLookup.mockReturnValue({ concepts: [], conceptLookupError: null, isLoadingConcepts: false });
-    mockUseConceptId.mockReturnValue({
-      concept: null,
-      conceptName: null,
-      conceptNameLookupError: null,
-      isLoadingConcept: false,
-    });
-    const user = userEvent.setup();
-    renderComponent();
-
-    const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
-    await user.click(searchInput);
-    await user.type(searchInput, 'Does not exist');
-
-    expect(screen.getByText(/no concepts were found that match/i)).toBeInTheDocument();
-    expect(screen.getByText(/can't find a concept\?/i)).toBeInTheDocument();
-    expect(
-      screen.getByRole('link', {
-        name: /search in ocl/i,
-      }),
-    ).toBeInTheDocument();
-  });
-
-  it('shows loading spinner when concept is loading', async () => {
-    mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: true });
-    mockUseConceptId.mockReturnValue({
-      concept: null,
-      conceptName: null,
-      conceptNameLookupError: null,
-      isLoadingConcept: false,
-    });
-    const user = userEvent.setup();
-    renderComponent();
-
-    const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
-    await user.click(searchInput);
-    await user.type(searchInput, 'Does not exist');
-    expect(screen.getByText(/searching\.\.\./i)).toBeInTheDocument();
-  });
-
-  it('displays an error message if the searched concept cannot be found', async () => {
-    mockUseConceptLookup.mockReturnValue({ concepts: [], conceptLookupError: Error(), isLoadingConcepts: false });
-    mockUseConceptId.mockReturnValue({
-      concept: null,
-      conceptName: null,
-      conceptNameLookupError: null,
-      isLoadingConcept: false,
-    });
-    renderComponent();
-
-    expect(screen.getByText(/error fetching concepts/i)).toBeInTheDocument();
-    expect(screen.getByText(/please try again\./i)).toBeInTheDocument();
   });
 
   it('sets the date picker format to the concept date picker type', async () => {
@@ -201,7 +138,7 @@ describe('ObsTypeQuestion', () => {
     });
   });
 
-  it('loads the concept details along with the selected answer when editing a question', async () => {
+  it('loads the selected concept details', async () => {
     formField.questionOptions = {
       rendering: 'select',
       concept: concepts[0].uuid,
@@ -214,7 +151,6 @@ describe('ObsTypeQuestion', () => {
       isLoadingConcept: false,
       conceptNameLookupError: null,
     });
-    const user = userEvent.setup();
     renderComponent();
 
     expect(
@@ -233,66 +169,6 @@ describe('ObsTypeQuestion', () => {
         name: /same\-as/i,
       }),
     ).toBeInTheDocument();
-
-    const answersMenu = screen.getByRole('combobox', {
-      name: /select answers to display/i,
-    });
-    expect(answersMenu).toBeInTheDocument();
-
-    await user.click(answersMenu);
-    expect(
-      screen.getByRole('option', {
-        name: /answer 1/i,
-      }),
-    ).toBeInTheDocument();
-    expect(
-      screen.getByRole('option', {
-        name: /answer 2/i,
-      }),
-    ).toBeInTheDocument();
-
-    expect(screen.getByTitle(/answer 1/i)).toBeInTheDocument();
-  });
-
-  it('shows loading spinner when loading the concept when editing the question', async () => {
-    mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: true });
-    mockUseConceptId.mockReturnValue({
-      concept: null,
-      conceptName: null,
-      conceptNameLookupError: null,
-      isLoadingConcept: true,
-    });
-    const user = userEvent.setup();
-    renderComponent();
-
-    expect(
-      screen.queryByRole('searchbox', {
-        name: /search for a backing concept/i,
-      }),
-    ).not.toBeInTheDocument();
-    expect(screen.getByText(/loading\.\.\./i)).toBeInTheDocument();
-  });
-
-  it('shows error if concept in question cannot be not when trying to edit', async () => {
-    mockUseConceptLookup.mockReturnValue({ concepts: [], conceptLookupError: null, isLoadingConcepts: false });
-    mockUseConceptId.mockReturnValue({
-      concept: null,
-      conceptName: null,
-      conceptNameLookupError: Error(),
-      isLoadingConcept: false,
-    });
-    formField.questionOptions = {
-      rendering: 'select',
-      concept: concepts[0].uuid,
-      answers: [{ label: 'Answer 1', concept: '1' }],
-    };
-    renderComponent();
-
-    expect(screen.getByText(/couldn't resolve concept name/i)).toBeInTheDocument();
-    expect(
-      screen.getByText(/the linked concept '\{\{conceptname\}\}' does not exist in your dictionary/i),
-    ).toBeInTheDocument();
-    expect(screen.getByText(/answer 1/i)).toBeInTheDocument();
   });
 });
 
diff --git a/src/components/interactive-builder/modals/question/question-form/question/question.component.tsx b/src/components/interactive-builder/modals/question/question-form/question/question.component.tsx
index a8bb16e6..0115330b 100644
--- a/src/components/interactive-builder/modals/question/question-form/question/question.component.tsx
+++ b/src/components/interactive-builder/modals/question/question-form/question/question.component.tsx
@@ -3,7 +3,7 @@ import { TextInput, Button, Select, SelectItem, RadioButtonGroup, RadioButton }
 import { useTranslation } from 'react-i18next';
 import RenderTypeComponent from '../rendering-types/rendering-type.component';
 import QuestionTypeComponent from '../question-types/question-type.component';
-import RequiredLabel from '../required-label/required-label.component';
+import RequiredLabel from '../common/required-label/required-label.component';
 import { useFormField } from '../../form-field-context';
 import type { FormField, RenderType } from '@openmrs/esm-form-engine-lib';
 import { questionTypes, renderTypeOptions, renderingTypes } from '@constants';
diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/index.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/index.tsx
index 4c3131ed..f0fdc123 100644
--- a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/index.tsx
+++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/index.tsx
@@ -5,3 +5,4 @@ export { default as TextArea } from './text-area/textarea.component';
 export { default as Toggle } from './toggle/toggle.component';
 export { default as UiSelectExtended } from './ui-select-extended/ui-select-extended.component';
 export { default as Markdown } from './markdown/markdown.component';
+export { default as SelectAnswers } from './select/select-answers.component';
diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.component.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.component.tsx
new file mode 100644
index 00000000..c2a4cffb
--- /dev/null
+++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.component.tsx
@@ -0,0 +1,184 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react';
+import { Tag, MultiSelect, Stack } from '@carbon/react';
+import { useTranslation } from 'react-i18next';
+import ConceptSearch from '../../../common/concept-search/concept-search.component';
+import { useFormField } from '../../../../form-field-context';
+import type { Concept } from '@types';
+import styles from './select.scss';
+
+interface AnswerItem {
+  id: string;
+  text: string;
+}
+
+const SelectAnswers: React.FC = () => {
+  const { t } = useTranslation();
+  const { formField, concept, setFormField } = useFormField();
+  const [selectedAnswers, setSelectedAnswers] = useState<Array<AnswerItem>>(
+    formField.questionOptions?.answers
+      ? formField.questionOptions.answers.map((answer) => ({
+          id: answer.concept,
+          text: answer.label,
+        }))
+      : [],
+  );
+  const [addedAnswers, setAddedAnswers] = useState<AnswerItem[]>([]);
+  const initiallySelectedAnswers = useRef(
+    formField?.questionOptions?.answers?.map((answer) => ({
+      id: answer.concept,
+      text: answer.label,
+    })),
+  );
+
+  const selectAnswers = useCallback(
+    ({ selectedItems }: { selectedItems: Array<AnswerItem> }) => {
+      const mappedAnswers = selectedItems.map((answer) => ({
+        concept: answer.id,
+        label: answer.text,
+      }));
+
+      setSelectedAnswers(selectedItems);
+
+      setFormField((prevField) => {
+        const currentAnswers = prevField.questionOptions?.answers || [];
+        if (JSON.stringify(currentAnswers) === JSON.stringify(mappedAnswers)) {
+          return prevField;
+        }
+        return {
+          ...prevField,
+          questionOptions: {
+            ...prevField.questionOptions,
+            answers: mappedAnswers,
+          },
+        };
+      });
+    },
+    [setFormField],
+  );
+
+  const handleSelectAdditionalAnswer = useCallback(
+    (concept: Concept) => {
+      const newAnswer = { id: concept.uuid, text: concept.display };
+      const answerExistsInSelected = selectedAnswers.some((answer) => answer.id === newAnswer.id);
+      const answerExistsInAdded = addedAnswers.some((answer) => answer.id === newAnswer.id);
+      if (!answerExistsInSelected && !answerExistsInAdded) {
+        setAddedAnswers((prevAnswers) => [...prevAnswers, newAnswer]);
+        setFormField((prevFormField) => {
+          const existingAnswers = prevFormField.questionOptions?.answers ?? [];
+          existingAnswers.push({ concept: concept.uuid, label: concept.display });
+          return {
+            ...prevFormField,
+            questionOptions: {
+              ...prevFormField.questionOptions,
+              answers: existingAnswers,
+            },
+          };
+        });
+      }
+    },
+    [selectedAnswers, addedAnswers, setFormField],
+  );
+
+  const handleDeleteAdditionalAnswer = useCallback(
+    (id: string) => {
+      setAddedAnswers((prevAnswers) => prevAnswers.filter((answer) => answer.id !== id));
+      setFormField((prevFormField) => {
+        const selectedAnswers = prevFormField.questionOptions?.answers ?? [];
+        return {
+          ...prevFormField,
+          questionOptions: {
+            ...prevFormField.questionOptions,
+            answers: selectedAnswers.filter((answer) => answer.concept !== id),
+          },
+        };
+      });
+    },
+    [setFormField],
+  );
+
+  const answerItems = useMemo(() => {
+    // Convert answers from the concept to items format
+    const conceptAnswerItems =
+      concept?.answers?.map((answer) => ({
+        id: answer.uuid,
+        text: answer.display,
+      })) ?? [];
+
+    const formFieldAnswers = formField.questionOptions?.answers ?? [];
+
+    // If no answers from concept but we have form field answers, use those
+    if (conceptAnswerItems.length === 0 && formFieldAnswers.length > 0) {
+      return formFieldAnswers.map((answer) => ({
+        id: answer.concept,
+        text: answer.label,
+      }));
+    }
+
+    // Merge concept answers with any additional form field answers
+    const additionalAnswers = formFieldAnswers
+      .filter((answer) => !conceptAnswerItems.some((item) => item.id === answer.concept))
+      .map((answer) => ({
+        id: answer.concept,
+        text: answer.label,
+      }));
+
+    return [...conceptAnswerItems, ...additionalAnswers];
+  }, [concept?.answers, formField.questionOptions?.answers]);
+
+  const convertAnswerItemsToString = useCallback((item: AnswerItem) => item.text, []);
+
+  return (
+    <Stack gap={5}>
+      {answerItems.length > 0 && (
+        <MultiSelect
+          className={styles.multiSelect}
+          direction="top"
+          id="selectAnswers"
+          items={answerItems}
+          itemToString={convertAnswerItemsToString}
+          onChange={selectAnswers}
+          size="md"
+          selectedItems={selectedAnswers}
+          initialSelectedItems={initiallySelectedAnswers}
+          titleText={t('selectAnswersToDisplay', 'Select answers to display')}
+        />
+      )}
+
+      {selectedAnswers.length > 0 && (
+        <div>
+          {selectedAnswers.map((answer) => (
+            <Tag className={styles.tag} key={answer.id} type={'blue'}>
+              {answer.text}
+            </Tag>
+          ))}
+        </div>
+      )}
+
+      {concept && concept.datatype?.name === 'Coded' && (
+        <>
+          <ConceptSearch
+            label={t('searchForAnswerConcept', 'Search for a concept to add as an answer')}
+            onSelectConcept={handleSelectAdditionalAnswer}
+          />
+          {addedAnswers.length > 0 ? (
+            <div>
+              {addedAnswers.map((answer) => (
+                <Tag className={styles.tag} key={answer.id} type={'blue'}>
+                  {answer.text}
+                  <button
+                    className={styles.conceptAnswerButton}
+                    onClick={() => handleDeleteAdditionalAnswer(answer.id)}
+                  >
+                    X
+                  </button>
+                </Tag>
+              ))}
+            </div>
+          ) : null}{' '}
+        </>
+      )}
+    </Stack>
+  );
+};
+
+export default SelectAnswers;
diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.scss b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.scss
new file mode 100644
index 00000000..dd82eaa5
--- /dev/null
+++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.scss
@@ -0,0 +1,25 @@
+@use '@carbon/colors';
+@use '@carbon/layout';
+
+.multiSelect {
+  :global(.cds--list-box__field--wrapper) {
+    align-items: center;
+    display: flex;
+    height: layout.$spacing-08;
+    justify-content: space-between;
+    margin-left: layout.$spacing-03;
+    width: 100%;
+  }
+}
+
+.tag {
+  margin-right: layout.$spacing-03;
+}
+
+.conceptAnswerButton {
+  margin-left: 5px;
+  color: colors.$black;
+  background: none;
+  border: none;
+  padding: 0;
+}
diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.test.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.test.tsx
new file mode 100644
index 00000000..53c12bcf
--- /dev/null
+++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.test.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import SelectAnswers from './select-answers.component';
+import { useConceptId } from '@hooks/useConceptId';
+import { useConceptLookup } from '@hooks/useConceptLookup';
+import { FormFieldProvider } from '../../../../form-field-context';
+import type { FormField } from '@openmrs/esm-form-engine-lib';
+import type { Concept } from '@types';
+import userEvent from '@testing-library/user-event';
+
+const formField: FormField = {
+  id: '1',
+  type: 'obs',
+  questionOptions: {
+    rendering: 'select',
+  },
+};
+const concept: Concept = {
+  uuid: '123',
+  display: 'Concept 1',
+  datatype: { uuid: '456', name: 'Coded' },
+  mappings: [{ display: 'CIEL:1606', conceptMapType: { display: 'SAME-AS' } }],
+  answers: [
+    { uuid: '1', display: 'Answer 1' },
+    { uuid: '2', display: 'Answer 2' },
+  ],
+};
+
+const concepts: Array<Concept> = [
+  concept,
+  {
+    uuid: '456',
+    display: 'Concept 2',
+    datatype: { uuid: '456', name: 'Date' },
+    mappings: [{ display: 'CIEL:1656', conceptMapType: { display: 'SAME-AS' } }],
+  },
+];
+
+const mockUseConceptLookup = jest.mocked(useConceptLookup);
+jest.mock('@hooks/useConceptLookup', () => ({
+  ...jest.requireActual('@hooks/useConceptLookup'),
+  useConceptLookup: jest.fn(),
+}));
+const mockUseConceptId = jest.mocked(useConceptId);
+jest.mock('@hooks/useConceptId', () => ({
+  ...jest.requireActual('@hooks/useConceptId'),
+  useConceptId: jest.fn(),
+}));
+
+describe('Select answers component', () => {
+  beforeEach(() => {
+    mockUseConceptLookup.mockReturnValue({ concepts: concepts, conceptLookupError: null, isLoadingConcepts: false });
+    mockUseConceptId.mockReturnValue({
+      concept: null,
+      conceptName: null,
+      conceptNameLookupError: null,
+      isLoadingConcept: false,
+    });
+  });
+  it('renders', () => {
+    renderComponent();
+    expect(screen.getByText(/select answers to display/i)).toBeInTheDocument();
+    expect(screen.getByText(/search for a concept to add as an answer/i)).toBeInTheDocument();
+    expect(
+      screen.getByRole('searchbox', {
+        name: /search for a backing concept/i,
+      }),
+    ).toBeInTheDocument();
+  });
+
+  it('lets user select answers provided by concept', async () => {
+    const user = userEvent.setup();
+    renderComponent();
+    const answersMenu = screen.getByRole('combobox', {
+      name: /select answers to display/i,
+    });
+    expect(answersMenu).toBeInTheDocument();
+
+    await user.click(answersMenu);
+    const answerOption1 = screen.getByRole('option', { name: /answer 1/i });
+    expect(answerOption1).toBeInTheDocument();
+    expect(screen.getByText(/answer 2/i)).toBeInTheDocument();
+    await user.click(answerOption1);
+
+    expect(screen.getByTitle(/answer 1/i)).toBeInTheDocument();
+    expect(
+      screen.getByRole('combobox', {
+        name: /select answers to display total items selected: 1,to clear selection, press delete or backspace/i,
+      }),
+    ).toBeInTheDocument();
+  });
+
+  it('lets users add additional answers if concept is of datatype coded', async () => {
+    const user = userEvent.setup();
+    renderComponent();
+
+    const searchInput = screen.getByRole('searchbox', { name: /search for a backing concept/i });
+    await user.click(searchInput);
+    await user.type(searchInput, 'Concept 1');
+    expect(
+      screen.getByRole('searchbox', {
+        name: /search for a backing concept/i,
+      }),
+    ).toHaveDisplayValue(/concept 1/i);
+    const additionalAnswerOption1 = screen.getByRole('menuitem', {
+      name: /concept 1/i,
+    });
+    expect(additionalAnswerOption1).toBeInTheDocument();
+    await user.click(additionalAnswerOption1);
+    expect(screen.getByText(/concept 1/i)).toBeInTheDocument();
+    expect(
+      screen.getByRole('button', {
+        name: /x/i,
+      }),
+    ).toBeInTheDocument();
+  });
+});
+
+function renderComponent() {
+  render(
+    <FormFieldProvider initialFormField={formField} selectedConcept={concept}>
+      <SelectAnswers />
+    </FormFieldProvider>,
+  );
+}
diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/rendering-type.component.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/rendering-type.component.tsx
index 0337beed..ba026f04 100644
--- a/src/components/interactive-builder/modals/question/question-form/rendering-types/rendering-type.component.tsx
+++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/rendering-type.component.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import { Date, Markdown, Number, Text, TextArea, Toggle, UiSelectExtended } from './inputs';
+import { Date, Markdown, Number, SelectAnswers, Text, TextArea, Toggle, UiSelectExtended } from './inputs';
 import { useFormField } from '../../form-field-context';
 import type { RenderType } from '@openmrs/esm-form-engine-lib';
 import { renderTypeOptions, renderingTypes } from '@constants';
@@ -13,6 +13,9 @@ const componentMap: Partial<Record<RenderType, React.FC>> = {
   date: Date,
   datetime: Date,
   markdown: Markdown,
+  select: SelectAnswers,
+  radio: SelectAnswers,
+  checkbox: SelectAnswers,
 };
 
 const RenderTypeComponent: React.FC = () => {