Skip to content

(feat) O3-4315: Add support for workspace-launcher rendering type #410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useCallback, useMemo } 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();

const { buttonLabel, workspaceName } = useMemo(
() => ({
buttonLabel: formField.questionOptions?.buttonLabel ?? '',
workspaceName: formField.questionOptions?.workspaceName ?? '',
}),
[formField.questionOptions?.buttonLabel, formField.questionOptions?.workspaceName],
);

const handleButtonLabelChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const updatedQuestion = {
...formField,
questionOptions: { ...formField.questionOptions, buttonLabel: event.target.value },
};
setFormField(updatedQuestion);
},
[formField, setFormField],
);

const handleWorkspaceNameChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const updatedQuestion = {
...formField,
questionOptions: { ...formField.questionOptions, workspaceName: event.target.value },
};
setFormField(updatedQuestion);
},
[formField, setFormField],
);

return (
<>
<TextInput
id="buttonLabel"
labelText={t('buttonLabel', 'Button Label')}
value={buttonLabel}
onChange={handleButtonLabelChange}
placeholder={t('buttonLabelPlaceholder', 'Enter text to display on the button')}
required
/>
<TextInput
id="workspaceName"
labelText={t('workspaceName', 'Workspace Name')}
value={workspaceName}
onChange={handleWorkspaceNameChange}
placeholder={t('workspaceNamePlaceholder', 'Enter the name of the workspace to launch')}
required
/>
</>
);
};

export default React.memo(WorkspaceLauncher);
Original file line number Diff line number Diff line change
@@ -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(
<FormFieldProvider initialFormField={formField}>
<WorkspaceLauncher />
</FormFieldProvider>,
);
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,6 +26,7 @@ const componentMap: Partial<Record<RenderType, React.FC>> = {
select: SelectAnswers,
radio: SelectAnswers,
checkbox: SelectAnswers,
'workspace-launcher': WorkspaceLauncher,
};

const RenderTypeComponent: React.FC = () => {
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -231,5 +233,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"
}