From dbba2f5e8b9ccbc13a9b16e73d42e1b4ceae1d4a Mon Sep 17 00:00:00 2001 From: Dwayne Date: Sun, 2 Mar 2025 02:07:49 +0530 Subject: [PATCH 1/4] Add support for workspace-launcher rendering type --- .../rendering-types/inputs/index.tsx | 1 + .../workspace-launcher.component.tsx | 44 +++++++++++++++++++ .../rendering-type.component.tsx | 13 +++++- src/types.ts | 2 + 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx 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 f0fdc1239..4c0ea5e3e 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 @@ -6,3 +6,4 @@ 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'; +export { default as WorkspaceLauncher } from './workspace-launcher/workspace-launcher.component'; diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx new file mode 100644 index 000000000..f9ad1e955 --- /dev/null +++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { TextInput } from '@carbon/react'; +import { useFormField } from '../../../../form-field-context'; + +const WorkspaceLauncher: React.FC = () => { + const { t } = useTranslation(); + const { formField, setFormField } = useFormField(); + + return ( + <> + ) => { + const updatedQuestion = { + ...formField, + questionOptions: { ...formField.questionOptions, buttonLabel: event.target.value }, + }; + setFormField(updatedQuestion); + }} + placeholder={t('buttonLabelPlaceholder', 'Enter text to display on the button')} + required + /> + ) => { + const updatedQuestion = { + ...formField, + questionOptions: { ...formField.questionOptions, workspaceName: event.target.value }, + }; + setFormField(updatedQuestion); + }} + placeholder={t('workspaceNamePlaceholder', 'Enter the name of the workspace to launch')} + required + /> + + ); +}; + +export default WorkspaceLauncher; 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 a2939666b..a4bdb2f9e 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,15 @@ import React from 'react'; -import { Date, Markdown, Number, SelectAnswers, Text, TextArea, Toggle, UiSelectExtended } from './inputs'; +import { + Date, + Markdown, + Number, + SelectAnswers, + Text, + TextArea, + Toggle, + UiSelectExtended, + WorkspaceLauncher, +} from './inputs'; import { useFormField } from '../../form-field-context'; import type { RenderType } from '@openmrs/esm-form-engine-lib'; import { renderTypeOptions, renderingTypes } from '@constants'; @@ -16,6 +26,7 @@ const componentMap: Partial> = { select: SelectAnswers, radio: SelectAnswers, checkbox: SelectAnswers, + 'workspace-launcher': WorkspaceLauncher, }; const RenderTypeComponent: React.FC = () => { diff --git a/src/types.ts b/src/types.ts index 1ace47b7c..d38484683 100644 --- a/src/types.ts +++ b/src/types.ts @@ -148,6 +148,8 @@ export interface QuestionOptions { labelTrue: string; labelFalse: string; }; + buttonLabel?: string; // Text to display on the button for workspace-launcher rendering type + workspaceName?: string; // Name of the workspace to launch for workspace-launcher rendering type } export interface Answer { From 37c00641acb96569b8b9b02d79e538f8fb6c0f43 Mon Sep 17 00:00:00 2001 From: Dwayne Date: Sun, 2 Mar 2025 02:10:47 +0530 Subject: [PATCH 2/4] Update en.json --- translations/en.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/translations/en.json b/translations/en.json index e5c1af4ae..002389ee5 100644 --- a/translations/en.json +++ b/translations/en.json @@ -8,6 +8,8 @@ "auditDetails": "Audit Details", "autogeneratedUuid": "UUID (auto-generated)", "backToDashboard": "Back to dashboard", + "buttonLabel": "Button Label", + "buttonLabelPlaceholder": "Enter text to display on the button", "calendarAndTimer": "Calendar and timer", "calendarOnly": "Calendar only", "cancel": "Cancel", @@ -227,5 +229,7 @@ "viewErrors": "View the errors in the interactive builder", "welcomeExplainer": "Add pages, sections and questions to your form. The Preview tab automatically updates as you build your form. For a detailed explanation of what constitutes an OpenMRS form schema, please read through the ", "welcomeHeading": "Interactive schema builder", + "workspaceName": "Workspace Name", + "workspaceNamePlaceholder": "Enter the name of the workspace to launch", "yes": "Yes" } From f54655ed3001fb530893d014daf08652a7618dbd Mon Sep 17 00:00:00 2001 From: Dwayne Date: Wed, 12 Mar 2025 20:02:56 +0530 Subject: [PATCH 3/4] improve workspace-launcher component formatting and test structure --- .../workspace-launcher.component.tsx | 54 ++++++--- .../workspace-launcher.test.tsx | 105 ++++++++++++++++++ 2 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx index f9ad1e955..619cb20d3 100644 --- a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx +++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { TextInput } from '@carbon/react'; import { useFormField } from '../../../../form-field-context'; @@ -7,33 +7,51 @@ const WorkspaceLauncher: React.FC = () => { const { t } = useTranslation(); const { formField, setFormField } = useFormField(); + const { buttonLabel, workspaceName } = useMemo( + () => ({ + buttonLabel: formField.questionOptions?.buttonLabel ?? '', + workspaceName: formField.questionOptions?.workspaceName ?? '', + }), + [formField.questionOptions?.buttonLabel, formField.questionOptions?.workspaceName], + ); + + const handleButtonLabelChange = useCallback( + (event: React.ChangeEvent) => { + const updatedQuestion = { + ...formField, + questionOptions: { ...formField.questionOptions, buttonLabel: event.target.value }, + }; + setFormField(updatedQuestion); + }, + [formField, setFormField], + ); + + const handleWorkspaceNameChange = useCallback( + (event: React.ChangeEvent) => { + const updatedQuestion = { + ...formField, + questionOptions: { ...formField.questionOptions, workspaceName: event.target.value }, + }; + setFormField(updatedQuestion); + }, + [formField, setFormField], + ); + return ( <> ) => { - const updatedQuestion = { - ...formField, - questionOptions: { ...formField.questionOptions, buttonLabel: event.target.value }, - }; - setFormField(updatedQuestion); - }} + value={buttonLabel} + onChange={handleButtonLabelChange} placeholder={t('buttonLabelPlaceholder', 'Enter text to display on the button')} required /> ) => { - const updatedQuestion = { - ...formField, - questionOptions: { ...formField.questionOptions, workspaceName: event.target.value }, - }; - setFormField(updatedQuestion); - }} + value={workspaceName} + onChange={handleWorkspaceNameChange} placeholder={t('workspaceNamePlaceholder', 'Enter the name of the workspace to launch')} required /> @@ -41,4 +59,4 @@ const WorkspaceLauncher: React.FC = () => { ); }; -export default WorkspaceLauncher; +export default React.memo(WorkspaceLauncher); diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx new file mode 100644 index 000000000..033fb6a28 --- /dev/null +++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import WorkspaceLauncher from './workspace-launcher.component'; +import { FormFieldProvider } from '../../../../form-field-context'; +import type { FormField } from '@openmrs/esm-form-engine-lib'; + +const mockSetFormField = jest.fn(); +const formField: FormField = { + type: 'obs', + questionOptions: { + rendering: 'workspace-launcher' as const, + buttonLabel: '', + workspaceName: '', + }, + id: '1', +}; + +jest.mock('../../../../form-field-context', () => ({ + ...jest.requireActual('../../../../form-field-context'), + useFormField: () => ({ formField, setFormField: mockSetFormField }), +})); + +describe('WorkspaceLauncher', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders workspace launcher inputs', () => { + renderWorkspaceLauncher(); + expect(screen.getByLabelText(/Button Label/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Workspace Name/i)).toBeInTheDocument(); + }); + + it('displays placeholder text for both inputs', () => { + renderWorkspaceLauncher(); + expect(screen.getByPlaceholderText(/Enter text to display on the button/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/Enter the name of the workspace to launch/i)).toBeInTheDocument(); + }); + + it('marks both inputs as required', () => { + renderWorkspaceLauncher(); + const buttonLabelInput = screen.getByLabelText(/Button Label/i); + const workspaceNameInput = screen.getByLabelText(/Workspace Name/i); + + expect(buttonLabelInput).toBeRequired(); + expect(workspaceNameInput).toBeRequired(); + }); + + it('updates button label when input changes', () => { + renderWorkspaceLauncher(); + const buttonLabelInput = screen.getByLabelText(/Button Label/i); + const newValue = 'Launch Patient Dashboard'; + + fireEvent.change(buttonLabelInput, { target: { value: newValue } }); + + expect(mockSetFormField).toHaveBeenCalledWith({ + ...formField, + questionOptions: { + ...formField.questionOptions, + buttonLabel: newValue, + }, + }); + }); + + it('updates workspace name when input changes', () => { + renderWorkspaceLauncher(); + const workspaceNameInput = screen.getByLabelText(/Workspace Name/i); + const newValue = 'patient-dashboard'; + + fireEvent.change(workspaceNameInput, { target: { value: newValue } }); + + expect(mockSetFormField).toHaveBeenCalledWith({ + ...formField, + questionOptions: { + ...formField.questionOptions, + workspaceName: newValue, + }, + }); + }); + + it('handles pasting text correctly', () => { + renderWorkspaceLauncher(); + const workspaceNameInput = screen.getByLabelText(/Workspace Name/i); + const textToPaste = 'pasted-workspace-name'; + + fireEvent.change(workspaceNameInput, { target: { value: textToPaste } }); + + expect(mockSetFormField).toHaveBeenCalledWith({ + ...formField, + questionOptions: { + ...formField.questionOptions, + workspaceName: textToPaste, + }, + }); + }); +}); + +function renderWorkspaceLauncher() { + render( + + + , + ); +} From 4f5d79d5a12669e965db6c8055491cacfd1f5011 Mon Sep 17 00:00:00 2001 From: Dwayne Date: Fri, 14 Mar 2025 22:43:50 +0530 Subject: [PATCH 4/4] fix: improve workspace-launcher code quality - Update setState to use callback pattern - Remove unnecessary test setup - Use userEvent instead of fireEvent in tests --- .../workspace-launcher.component.tsx | 22 ++++----- .../workspace-launcher.test.tsx | 48 +++++++------------ 2 files changed, 26 insertions(+), 44 deletions(-) diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx index 619cb20d3..1496b66dc 100644 --- a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx +++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.component.tsx @@ -17,24 +17,22 @@ const WorkspaceLauncher: React.FC = () => { const handleButtonLabelChange = useCallback( (event: React.ChangeEvent) => { - const updatedQuestion = { - ...formField, - questionOptions: { ...formField.questionOptions, buttonLabel: event.target.value }, - }; - setFormField(updatedQuestion); + setFormField((prevFormField) => ({ + ...prevFormField, + questionOptions: { ...prevFormField.questionOptions, buttonLabel: event.target.value }, + })); }, - [formField, setFormField], + [setFormField], ); const handleWorkspaceNameChange = useCallback( (event: React.ChangeEvent) => { - const updatedQuestion = { - ...formField, - questionOptions: { ...formField.questionOptions, workspaceName: event.target.value }, - }; - setFormField(updatedQuestion); + setFormField((prevFormField) => ({ + ...prevFormField, + questionOptions: { ...prevFormField.questionOptions, workspaceName: event.target.value }, + })); }, - [formField, setFormField], + [setFormField], ); return ( diff --git a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx index 033fb6a28..f56481197 100644 --- a/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx +++ b/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/workspace-launcher/workspace-launcher.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import WorkspaceLauncher from './workspace-launcher.component'; import { FormFieldProvider } from '../../../../form-field-context'; @@ -22,10 +22,6 @@ jest.mock('../../../../form-field-context', () => ({ })); describe('WorkspaceLauncher', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('renders workspace launcher inputs', () => { renderWorkspaceLauncher(); expect(screen.getByLabelText(/Button Label/i)).toBeInTheDocument(); @@ -47,52 +43,40 @@ describe('WorkspaceLauncher', () => { expect(workspaceNameInput).toBeRequired(); }); - it('updates button label when input changes', () => { + it('updates button label when input changes', async () => { renderWorkspaceLauncher(); + const user = userEvent.setup(); const buttonLabelInput = screen.getByLabelText(/Button Label/i); const newValue = 'Launch Patient Dashboard'; - fireEvent.change(buttonLabelInput, { target: { value: newValue } }); + await user.clear(buttonLabelInput); + await user.type(buttonLabelInput, newValue); - expect(mockSetFormField).toHaveBeenCalledWith({ - ...formField, - questionOptions: { - ...formField.questionOptions, - buttonLabel: newValue, - }, - }); + expect(mockSetFormField).toHaveBeenCalled(); }); - it('updates workspace name when input changes', () => { + it('updates workspace name when input changes', async () => { renderWorkspaceLauncher(); + const user = userEvent.setup(); const workspaceNameInput = screen.getByLabelText(/Workspace Name/i); const newValue = 'patient-dashboard'; - fireEvent.change(workspaceNameInput, { target: { value: newValue } }); + await user.clear(workspaceNameInput); + await user.type(workspaceNameInput, newValue); - expect(mockSetFormField).toHaveBeenCalledWith({ - ...formField, - questionOptions: { - ...formField.questionOptions, - workspaceName: newValue, - }, - }); + expect(mockSetFormField).toHaveBeenCalled(); }); - it('handles pasting text correctly', () => { + it('handles pasting text correctly', async () => { renderWorkspaceLauncher(); + const user = userEvent.setup(); const workspaceNameInput = screen.getByLabelText(/Workspace Name/i); const textToPaste = 'pasted-workspace-name'; - fireEvent.change(workspaceNameInput, { target: { value: textToPaste } }); + await user.clear(workspaceNameInput); + await user.paste(textToPaste); - expect(mockSetFormField).toHaveBeenCalledWith({ - ...formField, - questionOptions: { - ...formField.questionOptions, - workspaceName: textToPaste, - }, - }); + expect(mockSetFormField).toHaveBeenCalled(); }); });