Skip to content

Commit 6ad083b

Browse files
authored
fix(metadata-sidebar): handle onCancel action for editing templates (#3669)
* fix(metadata-sidebar): Fix selecting and cancelling templates * fix(metadata-sidebar): Add new MetadataInstanceEditor props in tests * feat(metadata-sidebar): Add storybook test for cancelling template edit * feat(metadata-sidebar): add tests for onCancel and onUnsavedChangesModalCancel action * fix(metadata-editor): fix failing tests * fix(metadata-sidebar): Fix attribute checks in tests * fix(metadata-editor): Fix test warnings * fix(metadata-editor): Remove unnecessary mock
1 parent c521c11 commit 6ad083b

4 files changed

Lines changed: 129 additions & 29 deletions

File tree

‎src/elements/content-sidebar/MetadataInstanceEditor.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface MetadataInstanceEditorProps {
1616
template: MetadataTemplateInstance;
1717
onSubmit: (values: FormValues, operations: JSONPatchOperations) => Promise<void>;
1818
setIsUnsavedChangesModalOpen: (isUnsavedChangesModalOpen: boolean) => void;
19+
onUnsavedChangesModalCancel: () => void;
1920
}
2021

2122
const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
@@ -27,6 +28,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
2728
setIsUnsavedChangesModalOpen,
2829
template,
2930
onCancel,
31+
onUnsavedChangesModalCancel,
3032
}) => {
3133
const handleCancel = () => {
3234
onCancel();
@@ -43,6 +45,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
4345
onSubmit={onSubmit}
4446
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
4547
onDelete={onDelete}
48+
onUnsavedChangesModalCancel={onUnsavedChangesModalCancel}
4649
/>
4750
</AutofillContextProvider>
4851
);

‎src/elements/content-sidebar/MetadataSidebarRedesign.tsx‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,40 @@ function MetadataSidebarRedesign({
9090
const [isDeleteButtonDisabled, setIsDeleteButtonDisabled] = React.useState<boolean>(false);
9191
const [selectedTemplates, setSelectedTemplates] =
9292
React.useState<Array<MetadataTemplateInstance | MetadataTemplate>>(templateInstances);
93+
const [pendingTemplateToEdit, setPendingTemplateToEdit] = React.useState<MetadataTemplateInstance | null>(null);
9394

9495
React.useEffect(() => {
9596
setSelectedTemplates(templateInstances);
9697
}, [templateInstances]);
9798

98-
const handleUnsavedChanges = () => {
99-
setIsUnsavedChangesModalOpen(true);
99+
const handleTemplateSelect = (selectedTemplate: MetadataTemplate) => {
100+
if (editingTemplate) {
101+
setPendingTemplateToEdit(convertTemplateToTemplateInstance(file, selectedTemplate));
102+
setIsUnsavedChangesModalOpen(true);
103+
} else {
104+
setSelectedTemplates([...selectedTemplates, selectedTemplate]);
105+
setEditingTemplate(convertTemplateToTemplateInstance(file, selectedTemplate));
106+
setIsDeleteButtonDisabled(true);
107+
}
100108
};
101109

102-
const handleTemplateSelect = (selectedTemplate: MetadataTemplate) => {
103-
setSelectedTemplates([...selectedTemplates, selectedTemplate]);
104-
setEditingTemplate(convertTemplateToTemplateInstance(file, selectedTemplate));
105-
setIsDeleteButtonDisabled(true);
110+
const handleCancel = () => {
111+
setEditingTemplate(null);
112+
setSelectedTemplates(templateInstances);
113+
};
114+
115+
const handleCancelUnsavedChanges = () => {
116+
// check if user tried to edit another template before unsaved changes modal
117+
if (pendingTemplateToEdit) {
118+
setEditingTemplate(pendingTemplateToEdit);
119+
setSelectedTemplates([...templateInstances, pendingTemplateToEdit]);
120+
setIsDeleteButtonDisabled(true);
121+
122+
setPendingTemplateToEdit(null);
123+
setIsUnsavedChangesModalOpen(false);
124+
} else {
125+
handleCancel();
126+
}
106127
};
107128

108129
const handleDeleteInstance = (metadataInstance: MetadataTemplateInstance) => {
@@ -128,9 +149,7 @@ function MetadataSidebarRedesign({
128149
<AddMetadataTemplateDropdown
129150
availableTemplates={templates}
130151
selectedTemplates={selectedTemplates as MetadataTemplate[]}
131-
onSelect={(selectedTemplate): void => {
132-
editingTemplate ? handleUnsavedChanges() : handleTemplateSelect(selectedTemplate);
133-
}}
152+
onSelect={handleTemplateSelect}
134153
/>
135154
);
136155

@@ -166,7 +185,8 @@ function MetadataSidebarRedesign({
166185
isBoxAiSuggestionsEnabled={isBoxAiSuggestionsEnabled}
167186
isDeleteButtonDisabled={isDeleteButtonDisabled}
168187
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
169-
onCancel={() => setEditingTemplate(null)}
188+
onCancel={handleCancel}
189+
onUnsavedChangesModalCancel={handleCancelUnsavedChanges}
170190
onSubmit={handleSubmit}
171191
onDelete={handleDeleteInstance}
172192
template={editingTemplate}

‎src/elements/content-sidebar/__tests__/MetadataInstanceEditor.test.tsx‎

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import React from 'react';
22
import { type MetadataTemplateInstance } from '@box/metadata-editor';
3+
import userEvent from '@testing-library/user-event';
34
import { screen, render } from '../../../test-utils/testing-library';
45
import MetadataInstanceEditor, { MetadataInstanceEditorProps } from '../MetadataInstanceEditor';
56

7+
const mockOnCancel = jest.fn();
8+
const mockOnUnsavedChangesModalCancel = jest.fn();
9+
const mockSetIsUnsavedChangesModalOpen = jest.fn();
10+
611
describe('MetadataInstanceEditor', () => {
712
const mockCustomMetadataTemplate: MetadataTemplateInstance = {
813
id: 'template-id',
@@ -19,6 +24,25 @@ describe('MetadataInstanceEditor', () => {
1924
displayName: 'Template Name',
2025
canEdit: true,
2126
};
27+
28+
const mockCustomMetadataTemplateWithField: MetadataTemplateInstance = {
29+
id: 'template-id',
30+
fields: [
31+
{
32+
id: '1',
33+
type: 'string',
34+
key: 'signature',
35+
hidden: false,
36+
displayName: 'Signature',
37+
},
38+
],
39+
scope: 'global',
40+
templateKey: 'customTemplate',
41+
type: 'template-id',
42+
hidden: false,
43+
canEdit: true,
44+
};
45+
2246
const mockMetadataTemplateInstance: MetadataTemplateInstance = {
2347
...mockCustomMetadataTemplate,
2448
displayName: 'Template Name',
@@ -29,10 +53,11 @@ describe('MetadataInstanceEditor', () => {
2953
isDeleteButtonDisabled: false,
3054
isUnsavedChangesModalOpen: false,
3155
template: mockMetadataTemplate,
32-
onCancel: jest.fn(),
56+
onCancel: mockOnCancel,
3357
onDelete: jest.fn(),
3458
onSubmit: jest.fn(),
35-
setIsUnsavedChangesModalOpen: jest.fn(),
59+
setIsUnsavedChangesModalOpen: mockSetIsUnsavedChangesModalOpen,
60+
onUnsavedChangesModalCancel: mockOnUnsavedChangesModalCancel,
3661
};
3762

3863
test('should render MetadataInstanceForm with correct props', () => {
@@ -50,25 +75,11 @@ describe('MetadataInstanceEditor', () => {
5075
expect(templateHeader).toBeInTheDocument();
5176
});
5277

53-
test('should render UnsavedChangesModal if isUnsavedChangesModalOpen is true', () => {
54-
// Mock window.matchMedia to simulate media query behavior for this test,
55-
// as the UnsavedChangesModal component relies on it.
56-
Object.defineProperty(window, 'matchMedia', {
57-
writable: true,
58-
value: jest.fn().mockImplementation(query => ({
59-
matches: false,
60-
media: query,
61-
onchange: null,
62-
addEventListener: jest.fn(),
63-
removeEventListener: jest.fn(),
64-
dispatchEvent: jest.fn(),
65-
})),
66-
});
67-
78+
test('should render UnsavedChangesModal if isUnsavedChangesModalOpen is true', async () => {
6879
const props = { ...defaultProps, isUnsavedChangesModalOpen: true };
69-
render(<MetadataInstanceEditor {...props} />);
80+
const { findByText } = render(<MetadataInstanceEditor {...props} />);
7081

71-
const unsavedChangesModal = screen.getByText('Unsaved Changes');
82+
const unsavedChangesModal = await findByText('Unsaved Changes');
7283
expect(unsavedChangesModal).toBeInTheDocument();
7384
});
7485

@@ -86,4 +97,40 @@ describe('MetadataInstanceEditor', () => {
8697
const deleteButton = screen.getByRole('button', { name: 'Delete' });
8798
expect(deleteButton).toBeEnabled();
8899
});
100+
101+
test('Should call onCancel when canceling editing', async () => {
102+
const props: MetadataInstanceEditorProps = { ...defaultProps, template: mockCustomMetadataTemplate };
103+
const { findByRole } = render(<MetadataInstanceEditor {...props} />);
104+
const cancelButton = await findByRole('button', { name: 'Cancel' });
105+
106+
await userEvent.click(cancelButton);
107+
108+
expect(mockOnCancel).toHaveBeenCalled();
109+
});
110+
111+
test('Should call onUnsavedChangesModalCancel instead onCancel when canceling through UnsavedChangesModal', async () => {
112+
const props: MetadataInstanceEditorProps = {
113+
...defaultProps,
114+
template: mockCustomMetadataTemplateWithField,
115+
};
116+
const { rerender, findByRole, findByText } = render(<MetadataInstanceEditor {...props} />);
117+
const input = await findByRole('textbox');
118+
const cancelButton = await findByRole('button', { name: 'Cancel' });
119+
120+
await userEvent.type(input, 'Lorem ipsum dolor.');
121+
await userEvent.click(cancelButton);
122+
123+
expect(mockOnCancel).not.toHaveBeenCalled();
124+
expect(mockSetIsUnsavedChangesModalOpen).toHaveBeenCalledWith(true);
125+
126+
rerender(<MetadataInstanceEditor {...props} isUnsavedChangesModalOpen={true} />);
127+
const unsavedChangesModal = await findByText('Unsaved Changes');
128+
129+
expect(unsavedChangesModal).toBeInTheDocument();
130+
const unsavedChangesModalCancelButton = await findByRole('button', { name: 'Cancel' });
131+
132+
await userEvent.click(unsavedChangesModalCancelButton);
133+
134+
expect(mockOnUnsavedChangesModalCancel).toHaveBeenCalled();
135+
});
89136
});

‎src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,33 @@ export const DeleteButtonIsEnabledWhenEditingMetadataTemplateInstance: StoryObj<
248248
expect(deleteButton).toBeEnabled();
249249
},
250250
};
251+
252+
export const MetadataInstanceEditorAddTemplateAgainAfterCancel: StoryObj<typeof MetadataSidebarRedesign> = {
253+
args: {
254+
fileId: '416047501580',
255+
metadataSidebarProps: defaultMetadataSidebarProps,
256+
},
257+
play: async ({ canvasElement }) => {
258+
const canvas = within(canvasElement);
259+
260+
const addTemplateButton = await canvas.findByRole('button', { name: 'Add template' }, { timeout: 5000 });
261+
262+
await userEvent.click(addTemplateButton);
263+
264+
const templateMetadataOption = canvas.getByRole('option', { name: 'My Template' });
265+
expect(templateMetadataOption).not.toHaveAttribute('aria-disabled');
266+
await userEvent.click(templateMetadataOption);
267+
268+
// Check if currently open template is disabled in dropdown
269+
await userEvent.click(addTemplateButton);
270+
const templateMetadataOptionDisabled = canvas.getByRole('option', { name: 'My Template' });
271+
expect(templateMetadataOptionDisabled).toHaveAttribute('aria-disabled');
272+
273+
// Check if template available again after cancelling
274+
const cancelButton = await canvas.findByRole('button', { name: 'Cancel' });
275+
await userEvent.click(cancelButton);
276+
await userEvent.click(addTemplateButton);
277+
const templateMetadataOptionEnabled = canvas.getByRole('option', { name: 'My Template' });
278+
expect(templateMetadataOptionEnabled).not.toHaveAttribute('aria-disabled');
279+
},
280+
};

0 commit comments

Comments
 (0)