Skip to content
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

fix(metadata-sidebar): handle onCancel action for editing templates #3669

Merged
merged 8 commits into from
Oct 1, 2024
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');
},
};
Loading