Skip to content

Commit

Permalink
(feat) O3-3638: Enable reordering questions across sections using dra…
Browse files Browse the repository at this point in the history
…g-and-drop (#344)

* enable drag and drop across sections

* Add some test coverage for the interactive builder

---------

Co-authored-by: Dennis Kigen <[email protected]>
  • Loading branch information
Twiineenock and denniskigen authored Sep 18, 2024
1 parent 22d7ff7 commit 675bdff
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 10 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @type {import('jest').Config} */
module.exports = {
clearMocks: true,
transform: {
'^.+\\.tsx?$': ['@swc/jest'],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,20 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({

// Get the destination information
const destination = over.id.toString().split('-');
const destinationPageIndex = parseInt(destination[2]);
const destinationSectionIndex = parseInt(destination[3]);
const destinationQuestionIndex = parseInt(destination[4]);

// Move the question within the same section
const questions = schema.pages[sourcePageIndex].sections[sourceSectionIndex].questions;
const questionToMove = questions[sourceQuestionIndex];
questions.splice(sourceQuestionIndex, 1);
questions.splice(destinationQuestionIndex, 0, questionToMove);
// Move the question within or across sections
const sourceQuestions = schema.pages[sourcePageIndex].sections[sourceSectionIndex].questions;
const destinationQuestions =
sourcePageIndex === destinationPageIndex && sourceSectionIndex === destinationSectionIndex
? sourceQuestions
: schema.pages[destinationPageIndex].sections[destinationSectionIndex].questions;

const questionToMove = sourceQuestions[sourceQuestionIndex];
sourceQuestions.splice(sourceQuestionIndex, 1);
destinationQuestions.splice(destinationQuestionIndex, 0, questionToMove);

const updatedSchema = {
...schema,
Expand All @@ -271,7 +278,12 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
if (sectionIndex === sourceSectionIndex) {
return {
...section,
questions: [...questions],
questions: [...sourceQuestions],
};
} else if (sectionIndex === destinationSectionIndex) {
return {
...section,
questions: [...destinationQuestions],
};
}
return section;
Expand Down Expand Up @@ -368,7 +380,7 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
>
{schema?.pages?.length
? schema.pages.map((page, pageIndex) => (
<div className={styles.editableFieldsContainer}>
<div className={styles.editableFieldsContainer} key={pageIndex}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div className={styles.editorContainer}>
<EditableValue
Expand Down Expand Up @@ -399,7 +411,7 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
) : null}
{page?.sections?.length ? (
page.sections?.map((section, sectionIndex) => (
<Accordion>
<Accordion key={sectionIndex}>
<AccordionItem title={section.label}>
<>
<div style={{ display: 'flex', alignItems: 'center' }}>
Expand All @@ -425,7 +437,10 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
{section.questions?.length ? (
section.questions.map((question, questionIndex) => {
return (
<Droppable id={`droppable-question-${pageIndex}-${sectionIndex}-${questionIndex}`}>
<Droppable
id={`droppable-question-${pageIndex}-${sectionIndex}-${questionIndex}`}
key={questionIndex}
>
<DraggableQuestion
handleDuplicateQuestion={duplicateQuestion}
key={question.id}
Expand All @@ -445,9 +460,10 @@ const InteractiveBuilder: React.FC<InteractiveBuilderProps> = ({
{getAnswerErrors(question.questionOptions.answers)?.length ? (
<div className={styles.answerErrors}>
<div>Answer Errors</div>
{getAnswerErrors(question.questionOptions.answers)?.map((error) => (
{getAnswerErrors(question.questionOptions.answers)?.map((error, index) => (
<div
className={styles.validationErrorMessage}
key={index}
>{`${error.field.label}: ${error.errorMessage}`}</div>
))}
</div>
Expand Down
113 changes: 113 additions & 0 deletions src/components/interactive-builder/interactive-builder.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { showModal } from '@openmrs/esm-framework';
import { type FormSchema } from '@openmrs/esm-form-engine-lib';
import { type Schema } from '../../types';
import InteractiveBuilder from './interactive-builder.component';

const mockShowModal = jest.mocked(showModal);

describe('InteractiveBuilder', () => {
it('renders the interactive builder', async () => {
const user = userEvent.setup();
renderInteractiveBuilder();

const startBuildingButton = screen.getByRole('button', { name: /start building/i });
expect(startBuildingButton).toBeInTheDocument();
await user.click(startBuildingButton);

expect(mockShowModal).toHaveBeenCalledTimes(1);
expect(mockShowModal).toHaveBeenCalledWith('new-form-modal', {
closeModal: expect.any(Function),
schema: {},
onSchemaChange: expect.any(Function),
});
});

it('populates the interactive builder with the provided schema', () => {
const dummySchema: FormSchema = {
encounterType: '',
name: 'Sample Form',
processor: 'EncounterFormProcessor',
referencedForms: [],
uuid: '',
version: '1.0',
pages: [
{
label: 'First Page',
sections: [
{
label: 'A Section',
isExpanded: 'true',
questions: [
{
id: 'sampleQuestion',
label: 'A Question of type obs that renders a text input',
type: 'obs',
questionOptions: {
rendering: 'text',
concept: 'a-system-defined-concept-uuid',
},
},
],
},
{
label: 'Another Section',
isExpanded: 'true',
questions: [
{
id: 'anotherSampleQuestion',
label: 'Another Question of type obs whose answers get rendered as radio inputs',
type: 'obs',
questionOptions: {
rendering: 'radio',
concept: 'system-defined-concept-uuid',
answers: [
{
concept: 'another-system-defined-concept-uuid',
label: 'Choice 1',
conceptMappings: [],
},
{
concept: 'yet-another-system-defined-concept-uuid',
label: 'Choice 2',
conceptMappings: [],
},
{
concept: 'yet-one-more-system-defined-concept-uuid',
label: 'Choice 3',
conceptMappings: [],
},
],
},
},
],
},
],
},
],
};

renderInteractiveBuilder({ schema: dummySchema });
expect(screen.getByRole('link', { name: /form builder documentation/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /add page/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: dummySchema.name })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: dummySchema.pages[0].label })).toBeInTheDocument();
expect(screen.getByRole('button', { name: dummySchema.pages[0].sections[0].label })).toBeInTheDocument();
expect(screen.getByRole('button', { name: dummySchema.pages[0].sections[1].label })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /delete page/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /add section/i })).toBeInTheDocument();
});
});

function renderInteractiveBuilder(props = {}) {
const defaultProps = {
isLoading: false,
onSchemaChange: jest.fn(),
schema: {} as Schema,
validationResponse: [],
};

render(<InteractiveBuilder {...defaultProps} {...props} />);
}

0 comments on commit 675bdff

Please sign in to comment.