Skip to content

Commit 1f85952

Browse files
authored
(feat) O3-5849: Improve the Workspace2 unsaved-changes confirmation modal (#1823)
1 parent eb16f91 commit 1f85952

10 files changed

Lines changed: 162 additions & 16 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@openmrs/esm-styleguide": patch
3+
"@openmrs/esm-translations": patch
4+
---
5+
6+
Improve the Workspace2 unsaved-changes confirmation modal. The title is now "Discard unsaved changes?", the body states that closing will discard the changes and distinguishes the single-workspace case (bolded workspace title inline) from the multiple-workspace case (count plus a bolded list), and the secondary action is "Keep editing" instead of "Cancel". The modal now uses the small Carbon size. The `closeWorkspaces2PromptTitle` and `closeWorkspaces2PromptBody` core translation keys are replaced by `discardUnsavedChangesPromptTitle`, `discardUnsavedChangesPromptBodySingle`, `discardUnsavedChangesPromptBodyMultiple`, and `keepEditing`.

packages/framework/esm-framework/docs/functions/getCoreTranslation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ invalid key to this function will result in a type error.
1919

2020
The translation key from the set of core translations.
2121

22-
`"cancel"` | `"change"` | `"close"` | `"error"` | `"unknown"` | `"state"` | `"Clinic"` | `"abnormalValue"` | `"actions"` | `"actionableNotification"` | `"address"` | `"age"` | `"batchActionItemSelected"` | `"batchActionsItemsSelected"` | `"closesActionableNotification"` | `"closeSnackbar"` | `"confirm"` | `"contactAdministratorIfIssuePersists"` | `"contactDetails"` | `"delete"` | `"discardChanges"` | `"edit"` | `"emptyStateText"` | `"errorCopy"` | `"errorLoadingLoginLocations"` | `"female"` | `"leftNavigation"` | `"loading"` | `"male"` | `"noResultsToDisplay"` | `"notAvailable"` | `"other"` | `"paginationItemsCount"` | `"paginationOfPages"` | `"patientAvatarAlt"` | `"patientIdentifierSticker"` | `"patientLists"` | `"patientPhotoAlt"` | `"patientPhotoPlaceholder"` | `"print"` | `"printError"` | `"printErrorExplainer"` | `"printIdentifierSticker"` | `"printing"` | `"recordNewEntry"` | `"relationships"` | `"resetOverrides"` | `"save"` | `"scriptLoadingFailed"` | `"scriptLoadingError"` | `"searchForLocation"` | `"seeAll"` | `"seeMoreLists"` | `"selectAll"` | `"sex"` | `"showLess"` | `"showMore"` | `"snackbarNotification"` | `"toggleDevTools"` | `"yearAbbreviation"` | `"yearsAbbreviation"` | `"closeWorkspaces2PromptTitle"` | `"closeWorkspaces2PromptBody"` | `"closeAllOpenedWorkspaces"` | `"closingAllWorkspacesPromptBody"` | `"closingAllWorkspacesPromptTitle"` | `"discard"` | `"hide"` | `"maximize"` | `"minimize"` | `"openAnyway"` | `"unsavedChangesInOpenedWorkspace"` | `"unsavedChangesInWorkspace"` | `"unsavedChangesTitleText"` | `"workspaceHeader"` | `"address1"` | `"address2"` | `"address3"` | `"address4"` | `"address5"` | `"address6"` | `"city"` | `"cityVillage"` | `"country"` | `"countyDistrict"` | `"district"` | `"postalCode"` | `"stateProvince"`
22+
`"cancel"` | `"change"` | `"close"` | `"error"` | `"unknown"` | `"state"` | `"Clinic"` | `"abnormalValue"` | `"actions"` | `"actionableNotification"` | `"address"` | `"age"` | `"batchActionItemSelected"` | `"batchActionsItemsSelected"` | `"closesActionableNotification"` | `"closeSnackbar"` | `"confirm"` | `"contactAdministratorIfIssuePersists"` | `"contactDetails"` | `"delete"` | `"discardChanges"` | `"edit"` | `"emptyStateText"` | `"errorCopy"` | `"errorLoadingLoginLocations"` | `"female"` | `"leftNavigation"` | `"loading"` | `"male"` | `"noResultsToDisplay"` | `"notAvailable"` | `"other"` | `"paginationItemsCount"` | `"paginationOfPages"` | `"patientAvatarAlt"` | `"patientIdentifierSticker"` | `"patientLists"` | `"patientPhotoAlt"` | `"patientPhotoPlaceholder"` | `"print"` | `"printError"` | `"printErrorExplainer"` | `"printIdentifierSticker"` | `"printing"` | `"recordNewEntry"` | `"relationships"` | `"resetOverrides"` | `"save"` | `"scriptLoadingFailed"` | `"scriptLoadingError"` | `"searchForLocation"` | `"seeAll"` | `"seeMoreLists"` | `"selectAll"` | `"sex"` | `"showLess"` | `"showMore"` | `"snackbarNotification"` | `"toggleDevTools"` | `"yearAbbreviation"` | `"yearsAbbreviation"` | `"discardUnsavedChangesPromptBodyMultiple"` | `"discardUnsavedChangesPromptBodySingle"` | `"discardUnsavedChangesPromptTitle"` | `"keepEditing"` | `"closeAllOpenedWorkspaces"` | `"closingAllWorkspacesPromptBody"` | `"closingAllWorkspacesPromptTitle"` | `"discard"` | `"hide"` | `"maximize"` | `"minimize"` | `"openAnyway"` | `"unsavedChangesInOpenedWorkspace"` | `"unsavedChangesInWorkspace"` | `"unsavedChangesTitleText"` | `"workspaceHeader"` | `"address1"` | `"address2"` | `"address3"` | `"address4"` | `"address5"` | `"address6"` | `"city"` | `"cityVillage"` | `"country"` | `"countyDistrict"` | `"district"` | `"postalCode"` | `"stateProvince"`
2323

2424
### defaultText?
2525

packages/framework/esm-framework/docs/functions/getRegisteredWorkspace2Names.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
> **getRegisteredWorkspace2Names**(): `string`[]
66
7-
Defined in: [packages/framework/esm-styleguide/src/workspaces2/workspace2.ts:600](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/workspaces2/workspace2.ts#L600)
7+
Defined in: [packages/framework/esm-styleguide/src/workspaces2/workspace2.ts:601](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/workspaces2/workspace2.ts#L601)
88

99
## Returns
1010

packages/framework/esm-framework/docs/functions/useWorkspace2Context.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
> **useWorkspace2Context**(): [`Workspace2DefinitionProps`](../interfaces/Workspace2DefinitionProps.md)\<`object`, `object`, `object`\>
66
7-
Defined in: [packages/framework/esm-styleguide/src/workspaces2/workspace2.ts:594](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/workspaces2/workspace2.ts#L594)
7+
Defined in: [packages/framework/esm-styleguide/src/workspaces2/workspace2.ts:595](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/workspaces2/workspace2.ts#L595)
88

99
Returns the react Context containing props passed into a workspace.
1010
This hook MUST be called inside a child of <Workspace2>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from 'react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
import { render, screen } from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
5+
import Workspace2ClosePromptModal from './workspace2-close-prompt.modal';
6+
7+
vi.mock('react-i18next', () => ({
8+
Trans: ({
9+
defaults,
10+
values,
11+
components,
12+
}: {
13+
defaults: string;
14+
values?: Record<string, string>;
15+
components?: Record<string, React.ReactElement>;
16+
}) => {
17+
let text = defaults;
18+
for (const [key, value] of Object.entries(values ?? {})) {
19+
text = text.replaceAll(`{{${key}}}`, value);
20+
}
21+
const parts = text.split(/<strong>(.*?)<\/strong>/);
22+
return (
23+
<>{parts.map((part, i) => (i % 2 === 1 ? React.cloneElement(components.strong, { key: i }, part) : part))}</>
24+
);
25+
},
26+
}));
27+
28+
describe('Workspace2ClosePromptModal', () => {
29+
it('renders a sentence with the bolded workspace title when one workspace is affected', () => {
30+
render(
31+
<Workspace2ClosePromptModal
32+
onConfirm={vi.fn()}
33+
onCancel={vi.fn()}
34+
affectedWorkspaceTitles={['Add drug order']}
35+
/>,
36+
);
37+
38+
expect(screen.getByText('Discard unsaved changes?')).toBeInTheDocument();
39+
expect(screen.getByText('Add drug order')).toBeInTheDocument();
40+
expect(screen.getByText('Add drug order').tagName).toBe('STRONG');
41+
expect(screen.getByText(/has unsaved changes\. Closing it will discard them\./)).toBeInTheDocument();
42+
expect(screen.queryByRole('list')).not.toBeInTheDocument();
43+
});
44+
45+
it('renders a count and a list of bolded workspace titles when multiple workspaces are affected', () => {
46+
render(
47+
<Workspace2ClosePromptModal
48+
onConfirm={vi.fn()}
49+
onCancel={vi.fn()}
50+
affectedWorkspaceTitles={['Add drug order', 'Visit note', 'Record vitals']}
51+
/>,
52+
);
53+
54+
expect(screen.getByText('Discard unsaved changes?')).toBeInTheDocument();
55+
expect(
56+
screen.getByText('3 workspaces have unsaved changes. Closing them will discard the changes:'),
57+
).toBeInTheDocument();
58+
const listItems = screen.getAllByRole('listitem');
59+
expect(listItems).toHaveLength(3);
60+
expect(listItems.map((item) => item.textContent)).toEqual(['Add drug order', 'Visit note', 'Record vitals']);
61+
for (const title of ['Add drug order', 'Visit note', 'Record vitals']) {
62+
expect(screen.getByText(title).tagName).toBe('STRONG');
63+
}
64+
});
65+
66+
it('calls onCancel when clicking "Keep editing"', async () => {
67+
const user = userEvent.setup();
68+
const onCancel = vi.fn();
69+
render(
70+
<Workspace2ClosePromptModal
71+
onConfirm={vi.fn()}
72+
onCancel={onCancel}
73+
affectedWorkspaceTitles={['Add drug order']}
74+
/>,
75+
);
76+
77+
await user.click(screen.getByRole('button', { name: 'Keep editing' }));
78+
expect(onCancel).toHaveBeenCalledTimes(1);
79+
});
80+
81+
it('calls onCancel when clicking the header close button', async () => {
82+
const user = userEvent.setup();
83+
const onCancel = vi.fn();
84+
render(
85+
<Workspace2ClosePromptModal
86+
onConfirm={vi.fn()}
87+
onCancel={onCancel}
88+
affectedWorkspaceTitles={['Add drug order']}
89+
/>,
90+
);
91+
92+
await user.click(screen.getByRole('button', { name: /close/i }));
93+
expect(onCancel).toHaveBeenCalledTimes(1);
94+
});
95+
96+
it('calls onConfirm when clicking "Discard changes"', async () => {
97+
const user = userEvent.setup();
98+
const onConfirm = vi.fn();
99+
render(
100+
<Workspace2ClosePromptModal
101+
onConfirm={onConfirm}
102+
onCancel={vi.fn()}
103+
affectedWorkspaceTitles={['Add drug order']}
104+
/>,
105+
);
106+
107+
await user.click(screen.getByRole('button', { name: /discard changes/i }));
108+
expect(onConfirm).toHaveBeenCalledTimes(1);
109+
});
110+
});

packages/framework/esm-styleguide/src/workspaces2/workspace2-close-prompt.modal.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import { Trans } from 'react-i18next';
23
import { ModalHeader, ModalBody, ModalFooter, Button } from '@carbon/react';
34
import { getCoreTranslation } from '@openmrs/esm-translations';
45
import styles from './workspace2-close-prompt.module.scss';
@@ -19,18 +20,38 @@ const Workspace2ClosePromptModal: React.FC<WorkspaceUnsavedChangesModal> = ({
1920
}) => {
2021
return (
2122
<>
22-
<ModalHeader closeModal={onCancel} title={getCoreTranslation('closeWorkspaces2PromptTitle')} />
23+
<ModalHeader closeModal={onCancel} title={getCoreTranslation('discardUnsavedChangesPromptTitle')} />
2324
<ModalBody>
24-
<p>{getCoreTranslation('closeWorkspaces2PromptBody')}</p>
25-
<ul className={styles.workspaceList}>
26-
{affectedWorkspaceTitles.map((title) => (
27-
<li key={title}>{title}</li>
28-
))}
29-
</ul>
25+
{affectedWorkspaceTitles.length === 1 ? (
26+
<p>
27+
<Trans
28+
ns="core"
29+
i18nKey="discardUnsavedChangesPromptBodySingle"
30+
defaults="<strong>{{workspaceTitle}}</strong> has unsaved changes. Closing it will discard them."
31+
values={{ workspaceTitle: affectedWorkspaceTitles[0] }}
32+
components={{ strong: <strong /> }}
33+
/>
34+
</p>
35+
) : (
36+
<>
37+
<p>
38+
{getCoreTranslation('discardUnsavedChangesPromptBodyMultiple', undefined, {
39+
count: affectedWorkspaceTitles.length,
40+
})}
41+
</p>
42+
<ul className={styles.workspaceList}>
43+
{affectedWorkspaceTitles.map((title, i) => (
44+
<li key={i}>
45+
<strong>{title}</strong>
46+
</li>
47+
))}
48+
</ul>
49+
</>
50+
)}
3051
</ModalBody>
3152
<ModalFooter>
3253
<Button kind="secondary" onClick={onCancel}>
33-
{getCoreTranslation('cancel', 'Cancel')}
54+
{getCoreTranslation('keepEditing')}
3455
</Button>
3556
<Button kind="danger" onClick={onConfirm}>
3657
{getCoreTranslation('discardChanges', 'Discard changes')}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
@use '@carbon/layout';
22

33
.workspaceList {
4+
margin-top: layout.$spacing-03;
5+
46
li {
5-
margin-left: layout.$spacing-05;
7+
margin-inline-start: layout.$spacing-05;
68
list-style: initial;
79
}
810
}

packages/framework/esm-styleguide/src/workspaces2/workspace2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ export function promptForClosingWorkspaces(promptReason: PromptReason): Promise<
410410

411411
return new Promise((resolve) => {
412412
const dispose = showModal('workspace2-close-prompt', {
413+
size: 'sm',
413414
onConfirm: () => {
414415
dispose();
415416
resolve(true);

packages/framework/esm-translations/src/translations.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ const workspaceTranslations = {
3636
};
3737

3838
const workspace2Translations = {
39-
closeWorkspaces2PromptTitle: 'Close workspace(s)',
40-
closeWorkspaces2PromptBody: 'You are about to close the following workspace(s), which might have unsaved changes:',
39+
discardUnsavedChangesPromptBodyMultiple:
40+
'{{count}} workspaces have unsaved changes. Closing them will discard the changes:',
41+
discardUnsavedChangesPromptBodySingle:
42+
'<strong>{{workspaceTitle}}</strong> has unsaved changes. Closing it will discard them.',
43+
discardUnsavedChangesPromptTitle: 'Discard unsaved changes?',
44+
keepEditing: 'Keep editing',
4145
};
4246

4347
export const coreTranslations = {

packages/framework/esm-translations/translations/en.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
"closeAllOpenedWorkspaces": "Discard changes in {{count}} workspaces",
2222
"closesActionableNotification": "Close actionable notification",
2323
"closeSnackbar": "Close snackbar",
24-
"closeWorkspaces2PromptBody": "You are about to close the following workspace(s), which might have unsaved changes:",
25-
"closeWorkspaces2PromptTitle": "Close workspace(s)",
2624
"closingAllWorkspacesPromptBody": "There may be unsaved changes in the following workspaces. Do you want to discard changes in the following workspaces? {{workspaceNames}}",
2725
"closingAllWorkspacesPromptTitle": "You have unsaved changes",
2826
"confirm": "Confirm",
@@ -33,6 +31,9 @@
3331
"delete": "Delete",
3432
"discard": "Discard",
3533
"discardChanges": "Discard changes",
34+
"discardUnsavedChangesPromptBodyMultiple": "{{count}} workspaces have unsaved changes. Closing them will discard the changes:",
35+
"discardUnsavedChangesPromptBodySingle": "<strong>{{workspaceTitle}}</strong> has unsaved changes. Closing it will discard them.",
36+
"discardUnsavedChangesPromptTitle": "Discard unsaved changes?",
3637
"district": "District",
3738
"edit": "Edit",
3839
"emptyStateText": "There are no {{displayText}} to display",
@@ -41,6 +42,7 @@
4142
"errorLoadingLoginLocations": "Unable to load login locations. Please try again or contact support if the problem persists.",
4243
"female": "Female",
4344
"hide": "Hide",
45+
"keepEditing": "Keep editing",
4446
"leftNavigation": "Left navigation",
4547
"loading": "Loading",
4648
"male": "Male",

0 commit comments

Comments
 (0)