Skip to content

Commit

Permalink
fix(metadata-sidebar): handle onCancel action for editing templates (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
JakubKida authored Oct 1, 2024
1 parent c521c11 commit 6ad083b
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 29 deletions.
3 changes: 3 additions & 0 deletions src/elements/content-sidebar/MetadataInstanceEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface MetadataInstanceEditorProps {
template: MetadataTemplateInstance;
onSubmit: (values: FormValues, operations: JSONPatchOperations) => Promise<void>;
setIsUnsavedChangesModalOpen: (isUnsavedChangesModalOpen: boolean) => void;
onUnsavedChangesModalCancel: () => void;
}

const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
Expand All @@ -27,6 +28,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
setIsUnsavedChangesModalOpen,
template,
onCancel,
onUnsavedChangesModalCancel,
}) => {
const handleCancel = () => {
onCancel();
Expand All @@ -43,6 +45,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
onSubmit={onSubmit}
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
onDelete={onDelete}
onUnsavedChangesModalCancel={onUnsavedChangesModalCancel}
/>
</AutofillContextProvider>
);
Expand Down
40 changes: 30 additions & 10 deletions src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,40 @@ function MetadataSidebarRedesign({
const [isDeleteButtonDisabled, setIsDeleteButtonDisabled] = React.useState<boolean>(false);
const [selectedTemplates, setSelectedTemplates] =
React.useState<Array<MetadataTemplateInstance | MetadataTemplate>>(templateInstances);
const [pendingTemplateToEdit, setPendingTemplateToEdit] = React.useState<MetadataTemplateInstance | null>(null);

React.useEffect(() => {
setSelectedTemplates(templateInstances);
}, [templateInstances]);

const handleUnsavedChanges = () => {
setIsUnsavedChangesModalOpen(true);
const handleTemplateSelect = (selectedTemplate: MetadataTemplate) => {
if (editingTemplate) {
setPendingTemplateToEdit(convertTemplateToTemplateInstance(file, selectedTemplate));
setIsUnsavedChangesModalOpen(true);
} else {
setSelectedTemplates([...selectedTemplates, selectedTemplate]);
setEditingTemplate(convertTemplateToTemplateInstance(file, selectedTemplate));
setIsDeleteButtonDisabled(true);
}
};

const handleTemplateSelect = (selectedTemplate: MetadataTemplate) => {
setSelectedTemplates([...selectedTemplates, selectedTemplate]);
setEditingTemplate(convertTemplateToTemplateInstance(file, selectedTemplate));
setIsDeleteButtonDisabled(true);
const handleCancel = () => {
setEditingTemplate(null);
setSelectedTemplates(templateInstances);
};

const handleCancelUnsavedChanges = () => {
// check if user tried to edit another template before unsaved changes modal
if (pendingTemplateToEdit) {
setEditingTemplate(pendingTemplateToEdit);
setSelectedTemplates([...templateInstances, pendingTemplateToEdit]);
setIsDeleteButtonDisabled(true);

setPendingTemplateToEdit(null);
setIsUnsavedChangesModalOpen(false);
} else {
handleCancel();
}
};

const handleDeleteInstance = (metadataInstance: MetadataTemplateInstance) => {
Expand All @@ -128,9 +149,7 @@ function MetadataSidebarRedesign({
<AddMetadataTemplateDropdown
availableTemplates={templates}
selectedTemplates={selectedTemplates as MetadataTemplate[]}
onSelect={(selectedTemplate): void => {
editingTemplate ? handleUnsavedChanges() : handleTemplateSelect(selectedTemplate);
}}
onSelect={handleTemplateSelect}
/>
);

Expand Down Expand Up @@ -166,7 +185,8 @@ function MetadataSidebarRedesign({
isBoxAiSuggestionsEnabled={isBoxAiSuggestionsEnabled}
isDeleteButtonDisabled={isDeleteButtonDisabled}
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
onCancel={() => setEditingTemplate(null)}
onCancel={handleCancel}
onUnsavedChangesModalCancel={handleCancelUnsavedChanges}
onSubmit={handleSubmit}
onDelete={handleDeleteInstance}
template={editingTemplate}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React from 'react';
import { type MetadataTemplateInstance } from '@box/metadata-editor';
import userEvent from '@testing-library/user-event';
import { screen, render } from '../../../test-utils/testing-library';
import MetadataInstanceEditor, { MetadataInstanceEditorProps } from '../MetadataInstanceEditor';

const mockOnCancel = jest.fn();
const mockOnUnsavedChangesModalCancel = jest.fn();
const mockSetIsUnsavedChangesModalOpen = jest.fn();

describe('MetadataInstanceEditor', () => {
const mockCustomMetadataTemplate: MetadataTemplateInstance = {
id: 'template-id',
Expand All @@ -19,6 +24,25 @@ describe('MetadataInstanceEditor', () => {
displayName: 'Template Name',
canEdit: true,
};

const mockCustomMetadataTemplateWithField: MetadataTemplateInstance = {
id: 'template-id',
fields: [
{
id: '1',
type: 'string',
key: 'signature',
hidden: false,
displayName: 'Signature',
},
],
scope: 'global',
templateKey: 'customTemplate',
type: 'template-id',
hidden: false,
canEdit: true,
};

const mockMetadataTemplateInstance: MetadataTemplateInstance = {
...mockCustomMetadataTemplate,
displayName: 'Template Name',
Expand All @@ -29,10 +53,11 @@ describe('MetadataInstanceEditor', () => {
isDeleteButtonDisabled: false,
isUnsavedChangesModalOpen: false,
template: mockMetadataTemplate,
onCancel: jest.fn(),
onCancel: mockOnCancel,
onDelete: jest.fn(),
onSubmit: jest.fn(),
setIsUnsavedChangesModalOpen: jest.fn(),
setIsUnsavedChangesModalOpen: mockSetIsUnsavedChangesModalOpen,
onUnsavedChangesModalCancel: mockOnUnsavedChangesModalCancel,
};

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

test('should render UnsavedChangesModal if isUnsavedChangesModalOpen is true', () => {
// Mock window.matchMedia to simulate media query behavior for this test,
// as the UnsavedChangesModal component relies on it.
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

test('should render UnsavedChangesModal if isUnsavedChangesModalOpen is true', async () => {
const props = { ...defaultProps, isUnsavedChangesModalOpen: true };
render(<MetadataInstanceEditor {...props} />);
const { findByText } = render(<MetadataInstanceEditor {...props} />);

const unsavedChangesModal = screen.getByText('Unsaved Changes');
const unsavedChangesModal = await findByText('Unsaved Changes');
expect(unsavedChangesModal).toBeInTheDocument();
});

Expand All @@ -86,4 +97,40 @@ describe('MetadataInstanceEditor', () => {
const deleteButton = screen.getByRole('button', { name: 'Delete' });
expect(deleteButton).toBeEnabled();
});

test('Should call onCancel when canceling editing', async () => {
const props: MetadataInstanceEditorProps = { ...defaultProps, template: mockCustomMetadataTemplate };
const { findByRole } = render(<MetadataInstanceEditor {...props} />);
const cancelButton = await findByRole('button', { name: 'Cancel' });

await userEvent.click(cancelButton);

expect(mockOnCancel).toHaveBeenCalled();
});

test('Should call onUnsavedChangesModalCancel instead onCancel when canceling through UnsavedChangesModal', async () => {
const props: MetadataInstanceEditorProps = {
...defaultProps,
template: mockCustomMetadataTemplateWithField,
};
const { rerender, findByRole, findByText } = render(<MetadataInstanceEditor {...props} />);
const input = await findByRole('textbox');
const cancelButton = await findByRole('button', { name: 'Cancel' });

await userEvent.type(input, 'Lorem ipsum dolor.');
await userEvent.click(cancelButton);

expect(mockOnCancel).not.toHaveBeenCalled();
expect(mockSetIsUnsavedChangesModalOpen).toHaveBeenCalledWith(true);

rerender(<MetadataInstanceEditor {...props} isUnsavedChangesModalOpen={true} />);
const unsavedChangesModal = await findByText('Unsaved Changes');

expect(unsavedChangesModal).toBeInTheDocument();
const unsavedChangesModalCancelButton = await findByRole('button', { name: 'Cancel' });

await userEvent.click(unsavedChangesModalCancelButton);

expect(mockOnUnsavedChangesModalCancel).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,33 @@ export const DeleteButtonIsEnabledWhenEditingMetadataTemplateInstance: StoryObj<
expect(deleteButton).toBeEnabled();
},
};

export const MetadataInstanceEditorAddTemplateAgainAfterCancel: StoryObj<typeof MetadataSidebarRedesign> = {
args: {
fileId: '416047501580',
metadataSidebarProps: defaultMetadataSidebarProps,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

const addTemplateButton = await canvas.findByRole('button', { name: 'Add template' }, { timeout: 5000 });

await userEvent.click(addTemplateButton);

const templateMetadataOption = canvas.getByRole('option', { name: 'My Template' });
expect(templateMetadataOption).not.toHaveAttribute('aria-disabled');
await userEvent.click(templateMetadataOption);

// Check if currently open template is disabled in dropdown
await userEvent.click(addTemplateButton);
const templateMetadataOptionDisabled = canvas.getByRole('option', { name: 'My Template' });
expect(templateMetadataOptionDisabled).toHaveAttribute('aria-disabled');

// Check if template available again after cancelling
const cancelButton = await canvas.findByRole('button', { name: 'Cancel' });
await userEvent.click(cancelButton);
await userEvent.click(addTemplateButton);
const templateMetadataOptionEnabled = canvas.getByRole('option', { name: 'My Template' });
expect(templateMetadataOptionEnabled).not.toHaveAttribute('aria-disabled');
},
};

0 comments on commit 6ad083b

Please sign in to comment.